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:
101
packages/editor-app/src-tauri/Cargo.lock
generated
101
packages/editor-app/src-tauri/Cargo.lock
generated
@@ -1100,6 +1100,8 @@ dependencies = [
|
||||
"futures-util",
|
||||
"glob",
|
||||
"image",
|
||||
"notify",
|
||||
"notify-debouncer-mini",
|
||||
"once_cell",
|
||||
"qrcode",
|
||||
"serde",
|
||||
@@ -1379,6 +1381,15 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fsevent-sys"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futf"
|
||||
version = "0.1.5"
|
||||
@@ -2205,6 +2216,26 @@ dependencies = [
|
||||
"cfb",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inotify"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fdd168d97690d0b8c412d6b6c10360277f4d7ee495c5d0d5d5fe0854923255cc"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"inotify-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inotify-sys"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inout"
|
||||
version = "0.1.4"
|
||||
@@ -2214,6 +2245,15 @@ dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "instant"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "interpolate_name"
|
||||
version = "0.2.4"
|
||||
@@ -2379,6 +2419,26 @@ dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kqueue"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a"
|
||||
dependencies = [
|
||||
"kqueue-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kqueue-sys"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kuchikiki"
|
||||
version = "0.8.8-speedreader"
|
||||
@@ -2603,6 +2663,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi 0.11.1+wasi-snapshot-preview1",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
@@ -2708,6 +2769,46 @@ version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
|
||||
|
||||
[[package]]
|
||||
name = "notify"
|
||||
version = "7.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c533b4c39709f9ba5005d8002048266593c1cfaf3c5f0739d5b8ab0c6c504009"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"filetime",
|
||||
"fsevent-sys",
|
||||
"inotify",
|
||||
"kqueue",
|
||||
"libc",
|
||||
"log",
|
||||
"mio",
|
||||
"notify-types",
|
||||
"walkdir",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notify-debouncer-mini"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aaa5a66d07ed97dce782be94dcf5ab4d1b457f4243f7566c7557f15cabc8c799"
|
||||
dependencies = [
|
||||
"log",
|
||||
"notify",
|
||||
"notify-types",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notify-types"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "585d3cb5e12e01aed9e8a1f70d5c6b5e86fe2a6e48fc8cd0b3e0b8df6f6eb174"
|
||||
dependencies = [
|
||||
"instant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.4.6"
|
||||
|
||||
@@ -34,6 +34,8 @@ once_cell = "1.19"
|
||||
urlencoding = "2.1"
|
||||
qrcode = "0.14"
|
||||
image = "0.25"
|
||||
notify = "7.0"
|
||||
notify-debouncer-mini = "0.5"
|
||||
|
||||
[profile.dev]
|
||||
incremental = true
|
||||
|
||||
455
packages/editor-app/src-tauri/src/commands/build.rs
Normal file
455
packages/editor-app/src-tauri/src/commands/build.rs
Normal file
@@ -0,0 +1,455 @@
|
||||
//! Build related commands.
|
||||
//! 构建相关命令。
|
||||
//!
|
||||
//! Provides file operations and compilation for build pipelines.
|
||||
//! 为构建管线提供文件操作和编译功能。
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
/// Build progress event.
|
||||
/// 构建进度事件。
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BuildProgressEvent {
|
||||
/// Progress percentage (0-100) | 进度百分比
|
||||
pub progress: u32,
|
||||
/// Current step message | 当前步骤消息
|
||||
pub message: String,
|
||||
/// Current step index | 当前步骤索引
|
||||
pub current_step: u32,
|
||||
/// Total steps | 总步骤数
|
||||
pub total_steps: u32,
|
||||
}
|
||||
|
||||
/// Clean and recreate output directory.
|
||||
/// 清理并重建输出目录。
|
||||
#[tauri::command]
|
||||
pub async fn prepare_build_directory(output_path: String) -> Result<(), String> {
|
||||
let path = Path::new(&output_path);
|
||||
|
||||
// Remove existing directory if exists | 如果存在则删除现有目录
|
||||
if path.exists() {
|
||||
fs::remove_dir_all(path)
|
||||
.map_err(|e| format!("Failed to clean output directory | 清理输出目录失败: {}", e))?;
|
||||
}
|
||||
|
||||
// Create fresh directory | 创建新目录
|
||||
fs::create_dir_all(path)
|
||||
.map_err(|e| format!("Failed to create output directory | 创建输出目录失败: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Copy directory recursively.
|
||||
/// 递归复制目录。
|
||||
#[tauri::command]
|
||||
pub async fn copy_directory(
|
||||
src: String,
|
||||
dst: String,
|
||||
patterns: Option<Vec<String>>,
|
||||
) -> Result<u32, String> {
|
||||
let src_path = Path::new(&src);
|
||||
let dst_path = Path::new(&dst);
|
||||
|
||||
if !src_path.exists() {
|
||||
return Err(format!("Source directory does not exist | 源目录不存在: {}", src));
|
||||
}
|
||||
|
||||
// Create destination directory | 创建目标目录
|
||||
fs::create_dir_all(dst_path)
|
||||
.map_err(|e| format!("Failed to create destination directory | 创建目标目录失败: {}", e))?;
|
||||
|
||||
let mut copied_count = 0u32;
|
||||
|
||||
// Recursively copy | 递归复制
|
||||
copy_dir_recursive(src_path, dst_path, &patterns, &mut copied_count)?;
|
||||
|
||||
Ok(copied_count)
|
||||
}
|
||||
|
||||
/// Helper function to copy directory recursively.
|
||||
/// 递归复制目录的辅助函数。
|
||||
fn copy_dir_recursive(
|
||||
src: &Path,
|
||||
dst: &Path,
|
||||
patterns: &Option<Vec<String>>,
|
||||
count: &mut u32,
|
||||
) -> Result<(), String> {
|
||||
for entry in fs::read_dir(src)
|
||||
.map_err(|e| format!("Failed to read directory | 读取目录失败: {}", e))?
|
||||
{
|
||||
let entry = entry.map_err(|e| format!("Failed to read entry | 读取条目失败: {}", e))?;
|
||||
let src_path = entry.path();
|
||||
let file_name = entry.file_name();
|
||||
let dst_path = dst.join(&file_name);
|
||||
|
||||
if src_path.is_dir() {
|
||||
// Skip hidden directories | 跳过隐藏目录
|
||||
if file_name.to_string_lossy().starts_with('.') {
|
||||
continue;
|
||||
}
|
||||
|
||||
fs::create_dir_all(&dst_path)
|
||||
.map_err(|e| format!("Failed to create directory | 创建目录失败: {}", e))?;
|
||||
copy_dir_recursive(&src_path, &dst_path, patterns, count)?;
|
||||
} else {
|
||||
// Check if file matches patterns | 检查文件是否匹配模式
|
||||
if let Some(ref pats) = patterns {
|
||||
let file_name_str = file_name.to_string_lossy();
|
||||
let matches = pats.iter().any(|p| {
|
||||
if p.starts_with("*.") {
|
||||
let ext = &p[2..];
|
||||
file_name_str.ends_with(&format!(".{}", ext))
|
||||
} else {
|
||||
file_name_str.contains(p)
|
||||
}
|
||||
});
|
||||
|
||||
if !matches {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
fs::copy(&src_path, &dst_path)
|
||||
.map_err(|e| format!("Failed to copy file | 复制文件失败: {} -> {}: {}",
|
||||
src_path.display(), dst_path.display(), e))?;
|
||||
*count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Bundle options for esbuild.
|
||||
/// esbuild 打包选项。
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BundleOptions {
|
||||
/// Entry files | 入口文件
|
||||
pub entry_points: Vec<String>,
|
||||
/// Output directory | 输出目录
|
||||
pub output_dir: String,
|
||||
/// Output format (esm or iife) | 输出格式
|
||||
pub format: String,
|
||||
/// Bundle name | 打包名称
|
||||
pub bundle_name: String,
|
||||
/// Whether to minify | 是否压缩
|
||||
pub minify: bool,
|
||||
/// Whether to generate source map | 是否生成 source map
|
||||
pub source_map: bool,
|
||||
/// External dependencies | 外部依赖
|
||||
pub external: Vec<String>,
|
||||
/// Project root for resolving imports | 项目根目录
|
||||
pub project_root: String,
|
||||
/// Define replacements | 宏定义替换
|
||||
pub define: Option<std::collections::HashMap<String, String>>,
|
||||
}
|
||||
|
||||
/// Bundle result.
|
||||
/// 打包结果。
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BundleResult {
|
||||
/// Whether bundling succeeded | 是否打包成功
|
||||
pub success: bool,
|
||||
/// Output file path | 输出文件路径
|
||||
pub output_file: Option<String>,
|
||||
/// Output file size in bytes | 输出文件大小(字节)
|
||||
pub output_size: Option<u64>,
|
||||
/// Error message if failed | 失败时的错误信息
|
||||
pub error: Option<String>,
|
||||
/// Warnings | 警告
|
||||
pub warnings: Vec<String>,
|
||||
}
|
||||
|
||||
/// Bundle JavaScript/TypeScript files using esbuild.
|
||||
/// 使用 esbuild 打包 JavaScript/TypeScript 文件。
|
||||
#[tauri::command]
|
||||
pub async fn bundle_scripts(options: BundleOptions) -> Result<BundleResult, String> {
|
||||
let esbuild_path = find_esbuild(&options.project_root)?;
|
||||
|
||||
// Build output file path | 构建输出文件路径
|
||||
let output_file = Path::new(&options.output_dir)
|
||||
.join(&options.bundle_name)
|
||||
.with_extension("js");
|
||||
|
||||
// Ensure output directory exists | 确保输出目录存在
|
||||
if let Some(parent) = output_file.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.map_err(|e| format!("Failed to create output directory | 创建输出目录失败: {}", e))?;
|
||||
}
|
||||
|
||||
// Build esbuild arguments | 构建 esbuild 参数
|
||||
let mut args: Vec<String> = options.entry_points.clone();
|
||||
|
||||
args.push("--bundle".to_string());
|
||||
args.push(format!("--outfile={}", output_file.display()));
|
||||
args.push(format!("--format={}", options.format));
|
||||
args.push("--platform=browser".to_string());
|
||||
args.push("--target=es2020".to_string());
|
||||
|
||||
if options.source_map {
|
||||
args.push("--sourcemap".to_string());
|
||||
}
|
||||
|
||||
if options.minify {
|
||||
args.push("--minify".to_string());
|
||||
}
|
||||
|
||||
for external in &options.external {
|
||||
args.push(format!("--external:{}", external));
|
||||
}
|
||||
|
||||
// Add define replacements | 添加宏定义替换
|
||||
if let Some(ref defines) = options.define {
|
||||
for (key, value) in defines {
|
||||
args.push(format!("--define:{}={}", key, value));
|
||||
}
|
||||
}
|
||||
|
||||
// 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() {
|
||||
// Get output file size | 获取输出文件大小
|
||||
let output_size = fs::metadata(&output_file)
|
||||
.map(|m| m.len())
|
||||
.ok();
|
||||
|
||||
// Parse warnings from stderr | 从 stderr 解析警告
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let warnings: Vec<String> = stderr
|
||||
.lines()
|
||||
.filter(|l| l.contains("warning"))
|
||||
.map(|l| l.to_string())
|
||||
.collect();
|
||||
|
||||
Ok(BundleResult {
|
||||
success: true,
|
||||
output_file: Some(output_file.to_string_lossy().to_string()),
|
||||
output_size,
|
||||
error: None,
|
||||
warnings,
|
||||
})
|
||||
} else {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
|
||||
Ok(BundleResult {
|
||||
success: false,
|
||||
output_file: None,
|
||||
output_size: None,
|
||||
error: Some(stderr.to_string()),
|
||||
warnings: vec![],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate HTML file from template.
|
||||
/// 从模板生成 HTML 文件。
|
||||
#[tauri::command]
|
||||
pub async fn generate_html(
|
||||
output_path: String,
|
||||
title: String,
|
||||
scripts: Vec<String>,
|
||||
body_content: Option<String>,
|
||||
) -> Result<(), String> {
|
||||
let scripts_html: String = scripts
|
||||
.iter()
|
||||
.map(|s| format!(r#" <script src="{}"></script>"#, s))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
|
||||
let body = body_content.unwrap_or_else(|| {
|
||||
r#" <canvas id="game-canvas" style="width: 100%; height: 100%;"></canvas>"#.to_string()
|
||||
});
|
||||
|
||||
let html = format!(
|
||||
r#"<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{}</title>
|
||||
<style>
|
||||
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
||||
html, body {{ width: 100%; height: 100%; overflow: hidden; background: #000; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{}
|
||||
{}
|
||||
</body>
|
||||
</html>"#,
|
||||
title, body, scripts_html
|
||||
);
|
||||
|
||||
// Ensure parent directory exists | 确保父目录存在
|
||||
let path = Path::new(&output_path);
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.map_err(|e| format!("Failed to create directory | 创建目录失败: {}", e))?;
|
||||
}
|
||||
|
||||
fs::write(&output_path, html)
|
||||
.map_err(|e| format!("Failed to write HTML file | 写入 HTML 文件失败: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get file size.
|
||||
/// 获取文件大小。
|
||||
#[tauri::command]
|
||||
pub async fn get_file_size(file_path: String) -> Result<u64, String> {
|
||||
fs::metadata(&file_path)
|
||||
.map(|m| m.len())
|
||||
.map_err(|e| format!("Failed to get file size | 获取文件大小失败: {}", e))
|
||||
}
|
||||
|
||||
/// Get directory size recursively.
|
||||
/// 递归获取目录大小。
|
||||
#[tauri::command]
|
||||
pub async fn get_directory_size(dir_path: String) -> Result<u64, String> {
|
||||
let path = Path::new(&dir_path);
|
||||
if !path.exists() {
|
||||
return Err(format!("Directory does not exist | 目录不存在: {}", dir_path));
|
||||
}
|
||||
|
||||
calculate_dir_size(path)
|
||||
}
|
||||
|
||||
/// Helper to calculate directory size.
|
||||
/// 计算目录大小的辅助函数。
|
||||
fn calculate_dir_size(path: &Path) -> Result<u64, String> {
|
||||
let mut total_size = 0u64;
|
||||
|
||||
for entry in fs::read_dir(path)
|
||||
.map_err(|e| format!("Failed to read directory | 读取目录失败: {}", e))?
|
||||
{
|
||||
let entry = entry.map_err(|e| format!("Failed to read entry | 读取条目失败: {}", e))?;
|
||||
let entry_path = entry.path();
|
||||
|
||||
if entry_path.is_dir() {
|
||||
total_size += calculate_dir_size(&entry_path)?;
|
||||
} else {
|
||||
total_size += fs::metadata(&entry_path)
|
||||
.map(|m| m.len())
|
||||
.unwrap_or(0);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(total_size)
|
||||
}
|
||||
|
||||
/// Find esbuild executable.
|
||||
/// 查找 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" };
|
||||
|
||||
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 | 未找到 esbuild".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Write JSON file.
|
||||
/// 写入 JSON 文件。
|
||||
#[tauri::command]
|
||||
pub async fn write_json_file(file_path: String, content: String) -> Result<(), String> {
|
||||
let path = Path::new(&file_path);
|
||||
|
||||
// Ensure parent directory exists | 确保父目录存在
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.map_err(|e| format!("Failed to create directory | 创建目录失败: {}", e))?;
|
||||
}
|
||||
|
||||
fs::write(&file_path, content)
|
||||
.map_err(|e| format!("Failed to write JSON file | 写入 JSON 文件失败: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// List files in directory with extension filter.
|
||||
/// 列出目录中指定扩展名的文件。
|
||||
#[tauri::command]
|
||||
pub async fn list_files_by_extension(
|
||||
dir_path: String,
|
||||
extensions: Vec<String>,
|
||||
recursive: bool,
|
||||
) -> Result<Vec<String>, String> {
|
||||
let path = Path::new(&dir_path);
|
||||
if !path.exists() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
let mut files = Vec::new();
|
||||
list_files_recursive(path, &extensions, recursive, &mut files)?;
|
||||
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
/// Helper to list files recursively.
|
||||
/// 递归列出文件的辅助函数。
|
||||
fn list_files_recursive(
|
||||
path: &Path,
|
||||
extensions: &[String],
|
||||
recursive: bool,
|
||||
files: &mut Vec<String>,
|
||||
) -> Result<(), String> {
|
||||
for entry in fs::read_dir(path)
|
||||
.map_err(|e| format!("Failed to read directory | 读取目录失败: {}", e))?
|
||||
{
|
||||
let entry = entry.map_err(|e| format!("Failed to read entry | 读取条目失败: {}", e))?;
|
||||
let entry_path = entry.path();
|
||||
|
||||
if entry_path.is_dir() {
|
||||
if recursive {
|
||||
list_files_recursive(&entry_path, extensions, recursive, files)?;
|
||||
}
|
||||
} else if let Some(ext) = entry_path.extension() {
|
||||
let ext_str = ext.to_string_lossy().to_lowercase();
|
||||
if extensions.iter().any(|e| e.to_lowercase() == ext_str) {
|
||||
files.push(entry_path.to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read binary file and return as base64.
|
||||
/// 读取二进制文件并返回 base64 编码。
|
||||
#[tauri::command]
|
||||
pub async fn read_binary_file_as_base64(path: String) -> Result<String, String> {
|
||||
use base64::{Engine as _, engine::general_purpose::STANDARD};
|
||||
|
||||
let bytes = fs::read(&path)
|
||||
.map_err(|e| format!("Failed to read binary file | 读取二进制文件失败: {}", e))?;
|
||||
|
||||
Ok(STANDARD.encode(&bytes))
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -1,17 +1,25 @@
|
||||
//! Command modules
|
||||
//! Command modules.
|
||||
//! 命令模块。
|
||||
//!
|
||||
//! All Tauri commands organized by domain.
|
||||
//! 所有按领域组织的 Tauri 命令。
|
||||
|
||||
pub mod build;
|
||||
pub mod compiler;
|
||||
pub mod dialog;
|
||||
pub mod file_system;
|
||||
pub mod modules;
|
||||
pub mod plugin;
|
||||
pub mod profiler;
|
||||
pub mod project;
|
||||
pub mod system;
|
||||
|
||||
// Re-export all commands for convenience
|
||||
// Re-export all commands for convenience | 重新导出所有命令以方便使用
|
||||
pub use build::*;
|
||||
pub use compiler::*;
|
||||
pub use dialog::*;
|
||||
pub use file_system::*;
|
||||
pub use modules::*;
|
||||
pub use plugin::*;
|
||||
pub use profiler::*;
|
||||
pub use project::*;
|
||||
|
||||
175
packages/editor-app/src-tauri/src/commands/modules.rs
Normal file
175
packages/editor-app/src-tauri/src/commands/modules.rs
Normal file
@@ -0,0 +1,175 @@
|
||||
//! 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.
|
||||
/// 获取引擎模块目录路径。
|
||||
///
|
||||
/// Uses compile-time CARGO_MANIFEST_DIR in dev mode to locate dist/engine.
|
||||
/// 在开发模式下使用编译时的 CARGO_MANIFEST_DIR 来定位 dist/engine。
|
||||
#[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 dev_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.map(|p| p.join("dist/engine"))
|
||||
.unwrap_or_else(|| PathBuf::from("dist/engine"));
|
||||
|
||||
if dev_path.exists() {
|
||||
println!("[modules] Using dev path: {:?}", dev_path);
|
||||
return Ok(dev_path);
|
||||
}
|
||||
|
||||
// Fallback: try current working directory
|
||||
// 回退:尝试当前工作目录
|
||||
let cwd_path = std::env::current_dir()
|
||||
.map(|p| p.join("dist/engine"))
|
||||
.unwrap_or_else(|_| PathBuf::from("dist/engine"));
|
||||
|
||||
if cwd_path.exists() {
|
||||
println!("[modules] Using cwd path: {:?}", cwd_path);
|
||||
return Ok(cwd_path);
|
||||
}
|
||||
|
||||
return Err(format!(
|
||||
"Engine modules directory not found in dev mode. Tried: {:?}, {:?}. Run 'pnpm copy-modules' first.",
|
||||
dev_path, cwd_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())
|
||||
}
|
||||
@@ -12,14 +12,15 @@ use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tauri::Manager;
|
||||
|
||||
use state::{ProfilerState, ProjectPaths};
|
||||
use state::{ProfilerState, ProjectPaths, ScriptWatcherState};
|
||||
|
||||
fn main() {
|
||||
// Initialize shared state
|
||||
// Initialize shared state | 初始化共享状态
|
||||
let project_paths: ProjectPaths = Arc::new(Mutex::new(HashMap::new()));
|
||||
let project_paths_for_protocol = Arc::clone(&project_paths);
|
||||
|
||||
let profiler_state = ProfilerState::new();
|
||||
let script_watcher_state = ScriptWatcherState::new();
|
||||
|
||||
// Build and run the Tauri application
|
||||
tauri::Builder::default()
|
||||
@@ -34,10 +35,11 @@ fn main() {
|
||||
.register_uri_scheme_protocol("project", move |_app, request| {
|
||||
handle_project_protocol(request, &project_paths_for_protocol)
|
||||
})
|
||||
// Setup application state
|
||||
// Setup application state | 设置应用状态
|
||||
.setup(move |app| {
|
||||
app.manage(project_paths);
|
||||
app.manage(profiler_state);
|
||||
app.manage(script_watcher_state);
|
||||
Ok(())
|
||||
})
|
||||
// Register all commands
|
||||
@@ -85,6 +87,24 @@ fn main() {
|
||||
commands::stop_local_server,
|
||||
commands::get_local_ip,
|
||||
commands::generate_qrcode,
|
||||
// User code compilation | 用户代码编译
|
||||
commands::compile_typescript,
|
||||
commands::watch_scripts,
|
||||
commands::stop_watch_scripts,
|
||||
// Build commands | 构建命令
|
||||
commands::prepare_build_directory,
|
||||
commands::copy_directory,
|
||||
commands::bundle_scripts,
|
||||
commands::generate_html,
|
||||
commands::get_file_size,
|
||||
commands::get_directory_size,
|
||||
commands::write_json_file,
|
||||
commands::list_files_by_extension,
|
||||
commands::read_binary_file_as_base64,
|
||||
// Engine modules | 引擎模块
|
||||
commands::read_engine_modules_index,
|
||||
commands::read_module_manifest,
|
||||
commands::get_engine_modules_base_path,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
@@ -1,17 +1,52 @@
|
||||
//! Application state definitions
|
||||
//! Application state definitions.
|
||||
//! 应用状态定义。
|
||||
//!
|
||||
//! Centralized state management for the Tauri application.
|
||||
//! Tauri 应用的集中状态管理。
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::sync::Mutex as TokioMutex;
|
||||
use crate::profiler_ws::ProfilerServer;
|
||||
|
||||
/// Project paths state
|
||||
/// Project paths state.
|
||||
/// 项目路径状态。
|
||||
///
|
||||
/// Stores the current project path and other path-related information.
|
||||
/// 存储当前项目路径和其他路径相关信息。
|
||||
pub type ProjectPaths = Arc<Mutex<HashMap<String, String>>>;
|
||||
|
||||
/// Script watcher state.
|
||||
/// 脚本监视器状态。
|
||||
///
|
||||
/// Manages file watchers for hot reload functionality.
|
||||
/// 管理用于热重载功能的文件监视器。
|
||||
pub struct ScriptWatcherState {
|
||||
/// Active watchers keyed by project path | 按项目路径索引的活动监视器
|
||||
pub watchers: Arc<TokioMutex<HashMap<String, WatcherHandle>>>,
|
||||
}
|
||||
|
||||
/// Handle to a running file watcher.
|
||||
/// 正在运行的文件监视器句柄。
|
||||
pub struct WatcherHandle {
|
||||
/// Shutdown signal sender | 关闭信号发送器
|
||||
pub shutdown_tx: tokio::sync::oneshot::Sender<()>,
|
||||
}
|
||||
|
||||
impl ScriptWatcherState {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
watchers: Arc::new(TokioMutex::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ScriptWatcherState {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Profiler server state
|
||||
///
|
||||
/// Manages the lifecycle of the WebSocket profiler server.
|
||||
|
||||
Reference in New Issue
Block a user