feat(editor): 实现用户脚本编译加载和自动重编译 (#273)
This commit is contained in:
@@ -188,6 +188,46 @@ export class PlatformDetector {
|
||||
window.location !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测是否在 Tauri 桌面环境中运行
|
||||
* Check if running in Tauri desktop environment
|
||||
*
|
||||
* 同时支持 Tauri v1 (__TAURI__) 和 v2 (__TAURI_INTERNALS__)
|
||||
* Supports both Tauri v1 (__TAURI__) and v2 (__TAURI_INTERNALS__)
|
||||
*/
|
||||
public static isTauriEnvironment(): boolean {
|
||||
if (typeof window === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
// Tauri v1 uses __TAURI__, Tauri v2 uses __TAURI_INTERNALS__
|
||||
return '__TAURI__' in window || '__TAURI_INTERNALS__' in window;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测是否在编辑器环境中运行
|
||||
* Check if running in editor environment
|
||||
*
|
||||
* 包括 Tauri 桌面应用或带 __ESENGINE_EDITOR__ 标记的环境
|
||||
* Includes Tauri desktop app or environments marked with __ESENGINE_EDITOR__
|
||||
*/
|
||||
public static isEditorEnvironment(): boolean {
|
||||
if (typeof window === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Tauri desktop app | Tauri 桌面应用
|
||||
if (this.isTauriEnvironment()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Editor marker | 编辑器标记
|
||||
if ('__ESENGINE_EDITOR__' in window) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取详细的环境信息(用于调试)
|
||||
*/
|
||||
|
||||
@@ -24,12 +24,17 @@ pub struct CompileOptions {
|
||||
pub output_path: String,
|
||||
/// Output format (esm or iife) | 输出格式
|
||||
pub format: String,
|
||||
/// Global name for IIFE format | IIFE 格式的全局名称
|
||||
pub global_name: Option<String>,
|
||||
/// Whether to generate source map | 是否生成 source map
|
||||
pub source_map: bool,
|
||||
/// Whether to minify | 是否压缩
|
||||
pub minify: bool,
|
||||
/// External dependencies | 外部依赖
|
||||
pub external: Vec<String>,
|
||||
/// Module aliases (e.g., "@esengine/ecs-framework" -> "/path/to/shim.js")
|
||||
/// 模块别名
|
||||
pub alias: Option<std::collections::HashMap<String, String>>,
|
||||
/// Project root for resolving imports | 项目根目录用于解析导入
|
||||
pub project_root: String,
|
||||
}
|
||||
@@ -106,11 +111,27 @@ pub async fn compile_typescript(options: CompileOptions) -> Result<CompileResult
|
||||
args.push("--minify".to_string());
|
||||
}
|
||||
|
||||
// Add global name for IIFE format | 添加 IIFE 格式的全局名称
|
||||
if let Some(ref global_name) = options.global_name {
|
||||
args.push(format!("--global-name={}", global_name));
|
||||
}
|
||||
|
||||
// Add external dependencies | 添加外部依赖
|
||||
for external in &options.external {
|
||||
args.push(format!("--external:{}", external));
|
||||
}
|
||||
|
||||
// Add module aliases | 添加模块别名
|
||||
if let Some(ref aliases) = options.alias {
|
||||
for (from, to) in aliases {
|
||||
args.push(format!("--alias:{}={}", from, to));
|
||||
}
|
||||
}
|
||||
|
||||
// Build full command string for error reporting | 构建完整命令字符串用于错误报告
|
||||
let cmd_str = format!("{} {}", esbuild_path, args.join(" "));
|
||||
println!("[Compiler] Running: {}", cmd_str);
|
||||
|
||||
// Run esbuild | 运行 esbuild
|
||||
let output = Command::new(&esbuild_path)
|
||||
.args(&args)
|
||||
@@ -119,6 +140,7 @@ pub async fn compile_typescript(options: CompileOptions) -> Result<CompileResult
|
||||
.map_err(|e| format!("Failed to run esbuild | 运行 esbuild 失败: {}", e))?;
|
||||
|
||||
if output.status.success() {
|
||||
println!("[Compiler] Compilation successful: {}", options.output_path);
|
||||
Ok(CompileResult {
|
||||
success: true,
|
||||
errors: vec![],
|
||||
@@ -126,7 +148,37 @@ pub async fn compile_typescript(options: CompileOptions) -> Result<CompileResult
|
||||
})
|
||||
} else {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let errors = parse_esbuild_errors(&stderr);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
println!("[Compiler] Compilation failed");
|
||||
println!("[Compiler] stdout: {}", stdout);
|
||||
println!("[Compiler] stderr: {}", stderr);
|
||||
|
||||
// Try to parse errors from both stdout and stderr | 尝试从 stdout 和 stderr 解析错误
|
||||
let mut errors = parse_esbuild_errors(&stderr);
|
||||
if errors.is_empty() {
|
||||
errors = parse_esbuild_errors(&stdout);
|
||||
}
|
||||
|
||||
// If still no parsed errors, include the raw output and command | 如果仍然没有解析到错误,包含原始输出和命令
|
||||
if errors.is_empty() {
|
||||
let combined_output = if !stderr.is_empty() && !stdout.is_empty() {
|
||||
format!("stdout: {}\nstderr: {}", stdout.trim(), stderr.trim())
|
||||
} else if !stderr.is_empty() {
|
||||
stderr.trim().to_string()
|
||||
} else if !stdout.is_empty() {
|
||||
stdout.trim().to_string()
|
||||
} else {
|
||||
format!("Command failed: {}", cmd_str)
|
||||
};
|
||||
|
||||
errors.push(CompileError {
|
||||
message: combined_output,
|
||||
file: None,
|
||||
line: None,
|
||||
column: None,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(CompileResult {
|
||||
success: false,
|
||||
|
||||
@@ -82,10 +82,35 @@ pub fn delete_file(path: String) -> Result<(), String> {
|
||||
}
|
||||
|
||||
/// Delete directory (recursive)
|
||||
/// 递归删除目录
|
||||
#[tauri::command]
|
||||
pub fn delete_folder(path: String) -> Result<(), String> {
|
||||
fs::remove_dir_all(&path)
|
||||
.map_err(|e| format!("Failed to delete folder {}: {}", path, e))
|
||||
println!("[delete_folder] Attempting to delete: {}", path);
|
||||
|
||||
// Check if path exists
|
||||
// 检查路径是否存在
|
||||
let dir_path = std::path::Path::new(&path);
|
||||
if !dir_path.exists() {
|
||||
println!("[delete_folder] Path does not exist: {}", path);
|
||||
return Err(format!("Directory does not exist: {}", path));
|
||||
}
|
||||
|
||||
if !dir_path.is_dir() {
|
||||
println!("[delete_folder] Path is not a directory: {}", path);
|
||||
return Err(format!("Path is not a directory: {}", path));
|
||||
}
|
||||
|
||||
match fs::remove_dir_all(&path) {
|
||||
Ok(_) => {
|
||||
println!("[delete_folder] Successfully deleted: {}", path);
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
let error_msg = format!("Failed to delete folder {}: {}", path, e);
|
||||
eprintln!("[delete_folder] Error: {}", error_msg);
|
||||
Err(error_msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Rename or move file/folder
|
||||
|
||||
@@ -142,6 +142,109 @@ pub fn get_temp_dir() -> Result<String, String> {
|
||||
.ok_or_else(|| "Failed to get temp directory".to_string())
|
||||
}
|
||||
|
||||
/// 使用 where 命令查找可执行文件路径
|
||||
/// Use 'where' command to find executable path
|
||||
#[cfg(target_os = "windows")]
|
||||
fn find_command_path(cmd: &str) -> Option<String> {
|
||||
use std::process::Command as StdCommand;
|
||||
use std::path::Path;
|
||||
|
||||
// 使用 where 命令查找
|
||||
let output = StdCommand::new("where")
|
||||
.arg(cmd)
|
||||
.output()
|
||||
.ok()?;
|
||||
|
||||
if output.status.success() {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
// 取第一行结果(可能有多个匹配)
|
||||
if let Some(first_line) = stdout.lines().next() {
|
||||
let path = first_line.trim();
|
||||
if !path.is_empty() {
|
||||
let path_obj = Path::new(path);
|
||||
|
||||
// 检查是否是 bin 目录下的脚本(VSCode/Cursor 特征)
|
||||
// Check if it's a script in bin directory (VSCode/Cursor pattern)
|
||||
let is_bin_script = path_obj.parent()
|
||||
.map(|p| p.ends_with("bin"))
|
||||
.unwrap_or(false);
|
||||
|
||||
// 如果找到的是 .cmd 或 .bat,或者是 bin 目录下的脚本(where 可能不返回扩展名)
|
||||
// If found .cmd or .bat, or a script in bin directory (where may not return extension)
|
||||
let has_script_ext = path.ends_with(".cmd") || path.ends_with(".bat");
|
||||
|
||||
if has_script_ext || is_bin_script {
|
||||
// 尝试找 Code.exe (VSCode) 或 Cursor.exe 等
|
||||
// Try to find Code.exe (VSCode) or Cursor.exe etc.
|
||||
if let Some(bin_dir) = path_obj.parent() {
|
||||
if let Some(parent_dir) = bin_dir.parent() {
|
||||
// VSCode: bin/code.cmd -> Code.exe
|
||||
let exe_path = parent_dir.join("Code.exe");
|
||||
if exe_path.exists() {
|
||||
let exe_str = exe_path.to_string_lossy().to_string();
|
||||
println!("[find_command_path] Found {} exe at: {}", cmd, exe_str);
|
||||
return Some(exe_str);
|
||||
}
|
||||
|
||||
// Cursor: bin/cursor.cmd -> Cursor.exe
|
||||
let cursor_exe = parent_dir.join("Cursor.exe");
|
||||
if cursor_exe.exists() {
|
||||
let exe_str = cursor_exe.to_string_lossy().to_string();
|
||||
println!("[find_command_path] Found {} exe at: {}", cmd, exe_str);
|
||||
return Some(exe_str);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("[find_command_path] Found {} at: {}", cmd, path);
|
||||
return Some(path.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn find_command_path(cmd: &str) -> Option<String> {
|
||||
use std::process::Command as StdCommand;
|
||||
|
||||
let output = StdCommand::new("which")
|
||||
.arg(cmd)
|
||||
.output()
|
||||
.ok()?;
|
||||
|
||||
if output.status.success() {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let path = stdout.trim();
|
||||
if !path.is_empty() {
|
||||
return Some(path.to_string());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// 解析编辑器命令,返回实际可执行路径
|
||||
/// Resolve editor command to actual executable path
|
||||
fn resolve_editor_command(editor_command: &str) -> String {
|
||||
use std::path::Path;
|
||||
|
||||
// 如果命令已经是完整路径且存在,直接返回
|
||||
// If command is already a full path and exists, return it
|
||||
if Path::new(editor_command).exists() {
|
||||
return editor_command.to_string();
|
||||
}
|
||||
|
||||
// 使用系统命令查找可执行文件路径
|
||||
// Use system command to find executable path
|
||||
if let Some(path) = find_command_path(editor_command) {
|
||||
return path;
|
||||
}
|
||||
|
||||
// 回退到原始命令 | Fall back to original command
|
||||
editor_command.to_string()
|
||||
}
|
||||
|
||||
/// Open project folder with specified editor
|
||||
/// 使用指定编辑器打开项目文件夹
|
||||
///
|
||||
@@ -166,27 +269,49 @@ pub fn open_with_editor(
|
||||
return Err(format!("Project path does not exist: {}", normalized_project));
|
||||
}
|
||||
|
||||
// 解析编辑器命令到实际路径
|
||||
// Resolve editor command to actual path
|
||||
let resolved_command = resolve_editor_command(&editor_command);
|
||||
|
||||
println!(
|
||||
"[open_with_editor] editor: {}, project: {}, file: {:?}",
|
||||
editor_command, normalized_project, normalized_file
|
||||
"[open_with_editor] editor: {} -> {}, project: {}, file: {:?}",
|
||||
editor_command, resolved_command, normalized_project, normalized_file
|
||||
);
|
||||
|
||||
let mut cmd = Command::new(&editor_command);
|
||||
let mut cmd = Command::new(&resolved_command);
|
||||
|
||||
// Add project folder as first argument
|
||||
// VSCode/Cursor CLI 正确用法:
|
||||
// 1. 使用 --folder-uri 或直接传文件夹路径会打开新窗口
|
||||
// 2. 使用 --add 可以将文件夹添加到当前工作区
|
||||
// 3. 使用 --goto file:line:column 可以打开文件并定位
|
||||
//
|
||||
// VSCode/Cursor CLI correct usage:
|
||||
// 1. Use --folder-uri or pass folder path directly to open new window
|
||||
// 2. Use --add to add folder to current workspace
|
||||
// 3. Use --goto file:line:column to open file and navigate
|
||||
//
|
||||
// 正确命令格式: code <folder> <file>
|
||||
// 这会打开文件夹并同时打开文件
|
||||
// Correct command format: code <folder> <file>
|
||||
// This opens the folder and also opens the file
|
||||
|
||||
// Add project folder first
|
||||
// 先添加项目文件夹
|
||||
cmd.arg(&normalized_project);
|
||||
|
||||
// If a specific file is provided, add it as a second argument
|
||||
// Most editors support: editor <folder> <file>
|
||||
// If a specific file is provided, add it directly (not with -g)
|
||||
// VSCode will open the folder AND the file
|
||||
// 如果提供了文件,直接添加(不使用 -g)
|
||||
// VSCode 会同时打开文件夹和文件
|
||||
if let Some(ref file) = normalized_file {
|
||||
let file_path = Path::new(file);
|
||||
if file_path.exists() {
|
||||
let file_path_obj = Path::new(file);
|
||||
if file_path_obj.exists() {
|
||||
cmd.arg(file);
|
||||
}
|
||||
}
|
||||
|
||||
cmd.spawn()
|
||||
.map_err(|e| format!("Failed to open with editor '{}': {}", editor_command, e))?;
|
||||
.map_err(|e| format!("Failed to open with editor '{}': {}", resolved_command, e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -212,10 +337,13 @@ pub fn get_current_dir() -> Result<String, String> {
|
||||
.map_err(|e| format!("Failed to get current directory: {}", e))
|
||||
}
|
||||
|
||||
/// Copy type definitions to project for IDE intellisense
|
||||
/// 复制类型定义文件到项目以支持 IDE 智能感知
|
||||
/// Update project tsconfig.json with engine type paths
|
||||
/// 更新项目的 tsconfig.json,添加引擎类型路径
|
||||
///
|
||||
/// Scans dist/engine/ directory and adds paths for all modules with .d.ts files.
|
||||
/// 扫描 dist/engine/ 目录,为所有有 .d.ts 文件的模块添加路径。
|
||||
#[tauri::command]
|
||||
pub fn copy_type_definitions(app: AppHandle, project_path: String) -> Result<(), String> {
|
||||
pub fn update_project_tsconfig(app: AppHandle, project_path: String) -> Result<(), String> {
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
@@ -224,38 +352,68 @@ pub fn copy_type_definitions(app: AppHandle, project_path: String) -> Result<(),
|
||||
return Err(format!("Project path does not exist: {}", project_path));
|
||||
}
|
||||
|
||||
// Create types directory in project
|
||||
// 在项目中创建 types 目录
|
||||
let types_dir = project.join("types");
|
||||
if !types_dir.exists() {
|
||||
fs::create_dir_all(&types_dir)
|
||||
.map_err(|e| format!("Failed to create types directory: {}", e))?;
|
||||
// Get engine modules path (dist/engine/)
|
||||
// 获取引擎模块路径
|
||||
let engine_path = get_engine_modules_base_path_internal(&app)?;
|
||||
|
||||
// Read existing tsconfig.json
|
||||
// 读取现有的 tsconfig.json
|
||||
let tsconfig_path = project.join("tsconfig.json");
|
||||
let tsconfig_editor_path = project.join("tsconfig.editor.json");
|
||||
|
||||
// Update runtime tsconfig
|
||||
// 更新运行时 tsconfig
|
||||
if tsconfig_path.exists() {
|
||||
update_tsconfig_file(&tsconfig_path, &engine_path, false)?;
|
||||
println!("[update_project_tsconfig] Updated {}", tsconfig_path.display());
|
||||
}
|
||||
|
||||
// Get resource directory (where bundled files are)
|
||||
// 获取资源目录(打包文件所在位置)
|
||||
// Update editor tsconfig
|
||||
// 更新编辑器 tsconfig
|
||||
if tsconfig_editor_path.exists() {
|
||||
update_tsconfig_file(&tsconfig_editor_path, &engine_path, true)?;
|
||||
println!("[update_project_tsconfig] Updated {}", tsconfig_editor_path.display());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Internal function to get engine modules base path
|
||||
/// 内部函数:获取引擎模块基础路径
|
||||
fn get_engine_modules_base_path_internal(app: &AppHandle) -> Result<String, String> {
|
||||
let resource_dir = app.path()
|
||||
.resource_dir()
|
||||
.map_err(|e| format!("Failed to get resource directory: {}", e))?;
|
||||
|
||||
// Type definition files to copy
|
||||
// 要复制的类型定义文件
|
||||
// Format: (resource_path, workspace_path, dest_name)
|
||||
// 格式:(资源路径,工作区路径,目标文件名)
|
||||
// Note: resource_path is relative to Tauri resource dir (runtime/ is mapped to .)
|
||||
// 注意:resource_path 相对于 Tauri 资源目录(runtime/ 映射到 .)
|
||||
let type_files = [
|
||||
("types/ecs-framework.d.ts", "packages/core/dist/index.d.ts", "ecs-framework.d.ts"),
|
||||
("types/engine-core.d.ts", "packages/engine-core/dist/index.d.ts", "engine-core.d.ts"),
|
||||
];
|
||||
// Production mode: resource_dir/engine/
|
||||
// 生产模式
|
||||
let prod_path = resource_dir.join("engine");
|
||||
if prod_path.exists() {
|
||||
return prod_path.to_str()
|
||||
.map(|s| s.to_string())
|
||||
.ok_or_else(|| "Invalid path encoding".to_string());
|
||||
}
|
||||
|
||||
// Try to find workspace root (for development mode)
|
||||
// 尝试查找工作区根目录(用于开发模式)
|
||||
let workspace_root = std::env::current_dir()
|
||||
// Development mode: workspace/packages/editor-app/dist/engine/
|
||||
// 开发模式
|
||||
if let Some(ws_root) = find_workspace_root() {
|
||||
let dev_path = ws_root.join("packages").join("editor-app").join("dist").join("engine");
|
||||
if dev_path.exists() {
|
||||
return dev_path.to_str()
|
||||
.map(|s| s.to_string())
|
||||
.ok_or_else(|| "Invalid path encoding".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Err("Engine modules directory not found".to_string())
|
||||
}
|
||||
|
||||
/// Find workspace root directory
|
||||
/// 查找工作区根目录
|
||||
fn find_workspace_root() -> Option<std::path::PathBuf> {
|
||||
std::env::current_dir()
|
||||
.ok()
|
||||
.and_then(|cwd| {
|
||||
// Look for pnpm-workspace.yaml or package.json in parent directories
|
||||
// 在父目录中查找 pnpm-workspace.yaml 或 package.json
|
||||
let mut dir = cwd.as_path();
|
||||
loop {
|
||||
if dir.join("pnpm-workspace.yaml").exists() {
|
||||
@@ -266,40 +424,87 @@ pub fn copy_type_definitions(app: AppHandle, project_path: String) -> Result<(),
|
||||
None => return None,
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
let mut copied_count = 0;
|
||||
for (resource_relative, workspace_relative, dest_name) in type_files {
|
||||
let dest_path = types_dir.join(dest_name);
|
||||
/// Update a tsconfig file with engine paths
|
||||
/// 使用引擎路径更新 tsconfig 文件
|
||||
///
|
||||
/// Scans all subdirectories in engine_path for index.d.ts files.
|
||||
/// 扫描 engine_path 下所有子目录的 index.d.ts 文件。
|
||||
fn update_tsconfig_file(
|
||||
tsconfig_path: &std::path::Path,
|
||||
engine_path: &str,
|
||||
include_editor: bool,
|
||||
) -> Result<(), String> {
|
||||
use std::fs;
|
||||
|
||||
// Try resource directory first (production mode)
|
||||
// 首先尝试资源目录(生产模式)
|
||||
let src_path = resource_dir.join(resource_relative);
|
||||
if src_path.exists() {
|
||||
fs::copy(&src_path, &dest_path)
|
||||
.map_err(|e| format!("Failed to copy {}: {}", resource_relative, e))?;
|
||||
println!("[copy_type_definitions] Copied {} to {}", src_path.display(), dest_path.display());
|
||||
copied_count += 1;
|
||||
continue;
|
||||
}
|
||||
let content = fs::read_to_string(tsconfig_path)
|
||||
.map_err(|e| format!("Failed to read tsconfig: {}", e))?;
|
||||
|
||||
// Try workspace directory (development mode)
|
||||
// 尝试工作区目录(开发模式)
|
||||
if let Some(ref ws_root) = workspace_root {
|
||||
let ws_src_path = ws_root.join(workspace_relative);
|
||||
if ws_src_path.exists() {
|
||||
fs::copy(&ws_src_path, &dest_path)
|
||||
.map_err(|e| format!("Failed to copy {}: {}", workspace_relative, e))?;
|
||||
println!("[copy_type_definitions] Copied {} to {} (dev mode)", ws_src_path.display(), dest_path.display());
|
||||
copied_count += 1;
|
||||
let mut config: serde_json::Value = serde_json::from_str(&content)
|
||||
.map_err(|e| format!("Failed to parse tsconfig: {}", e))?;
|
||||
|
||||
// Normalize path for cross-platform compatibility
|
||||
// 规范化路径以实现跨平台兼容
|
||||
let engine_path_normalized = engine_path.replace('\\', "/");
|
||||
|
||||
// Build paths mapping by scanning engine modules directory
|
||||
// 通过扫描引擎模块目录构建路径映射
|
||||
let mut paths = serde_json::Map::new();
|
||||
let mut module_count = 0;
|
||||
|
||||
let engine_dir = std::path::Path::new(engine_path);
|
||||
if let Ok(entries) = fs::read_dir(engine_dir) {
|
||||
for entry in entries.flatten() {
|
||||
let module_path = entry.path();
|
||||
if !module_path.is_dir() {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
println!("[copy_type_definitions] {} not found, skipping", dest_name);
|
||||
let module_id = module_path.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or("");
|
||||
|
||||
// Skip editor modules for runtime tsconfig
|
||||
// 运行时 tsconfig 跳过编辑器模块
|
||||
if !include_editor && module_id.ends_with("-editor") {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for index.d.ts
|
||||
// 检查是否存在 index.d.ts
|
||||
let dts_path = module_path.join("index.d.ts");
|
||||
if dts_path.exists() {
|
||||
let module_name = format!("@esengine/{}", module_id);
|
||||
let dts_path_str = format!("{}/{}/index.d.ts", engine_path_normalized, module_id);
|
||||
paths.insert(module_name, serde_json::json!([dts_path_str]));
|
||||
module_count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("[copy_type_definitions] Copied {} type definition files to {}", copied_count, types_dir.display());
|
||||
println!("[update_tsconfig_file] Found {} modules with type definitions", module_count);
|
||||
|
||||
// Update compilerOptions.paths
|
||||
// 更新 compilerOptions.paths
|
||||
if let Some(compiler_options) = config.get_mut("compilerOptions") {
|
||||
if let Some(obj) = compiler_options.as_object_mut() {
|
||||
obj.insert("paths".to_string(), serde_json::Value::Object(paths));
|
||||
// Remove typeRoots since we're using paths
|
||||
// 移除 typeRoots,因为我们使用 paths
|
||||
obj.remove("typeRoots");
|
||||
}
|
||||
}
|
||||
|
||||
// Write back
|
||||
// 写回文件
|
||||
let output = serde_json::to_string_pretty(&config)
|
||||
.map_err(|e| format!("Failed to serialize tsconfig: {}", e))?;
|
||||
|
||||
fs::write(tsconfig_path, output)
|
||||
.map_err(|e| format!("Failed to write tsconfig: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ fn main() {
|
||||
commands::show_in_folder,
|
||||
commands::get_temp_dir,
|
||||
commands::open_with_editor,
|
||||
commands::copy_type_definitions,
|
||||
commands::update_project_tsconfig,
|
||||
commands::get_app_resource_dir,
|
||||
commands::get_current_dir,
|
||||
commands::start_local_server,
|
||||
@@ -156,7 +156,13 @@ fn handle_project_protocol(
|
||||
tauri::http::Response::builder()
|
||||
.status(200)
|
||||
.header("Content-Type", mime_type)
|
||||
// CORS headers for dynamic ES module imports | 动态 ES 模块导入所需的 CORS 头
|
||||
.header("Access-Control-Allow-Origin", "*")
|
||||
.header("Access-Control-Allow-Methods", "GET, OPTIONS")
|
||||
.header("Access-Control-Allow-Headers", "Content-Type")
|
||||
.header("Access-Control-Expose-Headers", "Content-Length")
|
||||
// Allow cross-origin script loading | 允许跨域脚本加载
|
||||
.header("Cross-Origin-Resource-Policy", "cross-origin")
|
||||
.body(content)
|
||||
.unwrap()
|
||||
}
|
||||
@@ -164,6 +170,7 @@ fn handle_project_protocol(
|
||||
eprintln!("Failed to read file {}: {}", file_path, e);
|
||||
tauri::http::Response::builder()
|
||||
.status(404)
|
||||
.header("Access-Control-Allow-Origin", "*")
|
||||
.body(Vec::new())
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
@@ -381,12 +381,12 @@ function App() {
|
||||
// 设置 Tauri project:// 协议的基础路径(用于加载插件等项目文件)
|
||||
await TauriAPI.setProjectBasePath(projectPath);
|
||||
|
||||
// 复制类型定义到项目,用于 IDE 智能感知
|
||||
// Copy type definitions to project for IDE intellisense
|
||||
// 更新项目 tsconfig,直接引用引擎类型定义
|
||||
// Update project tsconfig to reference engine type definitions directly
|
||||
try {
|
||||
await TauriAPI.copyTypeDefinitions(projectPath);
|
||||
await TauriAPI.updateProjectTsconfig(projectPath);
|
||||
} catch (e) {
|
||||
console.warn('[App] Failed to copy type definitions:', e);
|
||||
console.warn('[App] Failed to update project tsconfig:', e);
|
||||
}
|
||||
|
||||
const settings = SettingsService.getInstance();
|
||||
@@ -840,14 +840,17 @@ function App() {
|
||||
setStatus(t('header.status.ready'));
|
||||
}}
|
||||
onDeleteProject={async (projectPath) => {
|
||||
console.log('[App] onDeleteProject called with path:', projectPath);
|
||||
try {
|
||||
console.log('[App] Calling TauriAPI.deleteFolder...');
|
||||
await TauriAPI.deleteFolder(projectPath);
|
||||
console.log('[App] deleteFolder succeeded');
|
||||
// 删除成功后从列表中移除并触发重新渲染
|
||||
// Remove from list and trigger re-render after successful deletion
|
||||
settings.removeRecentProject(projectPath);
|
||||
setStatus(t('header.status.ready'));
|
||||
} catch (error) {
|
||||
console.error('Failed to delete project:', error);
|
||||
console.error('[App] Failed to delete project:', error);
|
||||
setErrorDialog({
|
||||
title: locale === 'zh' ? '删除项目失败' : 'Failed to Delete Project',
|
||||
message: locale === 'zh'
|
||||
|
||||
@@ -332,13 +332,17 @@ export class TauriAPI {
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制类型定义文件到项目
|
||||
* Copy type definition files to project for IDE intellisense
|
||||
* 更新项目的 tsconfig.json,添加引擎类型路径
|
||||
* Update project tsconfig.json with engine type paths
|
||||
*
|
||||
* This updates the tsconfig to point directly to engine's .d.ts files
|
||||
* instead of copying them to the project.
|
||||
* 这会更新 tsconfig 直接指向引擎的 .d.ts 文件,而不是复制到项目。
|
||||
*
|
||||
* @param projectPath 项目路径 | Project path
|
||||
*/
|
||||
static async copyTypeDefinitions(projectPath: string): Promise<void> {
|
||||
return await invoke<void>('copy_type_definitions', { projectPath });
|
||||
static async updateProjectTsconfig(projectPath: string): Promise<void> {
|
||||
return await invoke<void>('update_project_tsconfig', { projectPath });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -37,7 +37,9 @@ import {
|
||||
BuildService,
|
||||
WebBuildPipeline,
|
||||
WeChatBuildPipeline,
|
||||
moduleRegistry
|
||||
moduleRegistry,
|
||||
UserCodeService,
|
||||
UserCodeTarget
|
||||
} from '@esengine/editor-core';
|
||||
import { ViewportService } from '../../services/ViewportService';
|
||||
import { TransformComponent } from '@esengine/engine-core';
|
||||
@@ -78,6 +80,7 @@ import {
|
||||
import { TransformComponentInspector } from '../../components/inspectors/component-inspectors/TransformComponentInspector';
|
||||
import { buildFileSystem } from '../../services/BuildFileSystemService';
|
||||
import { TauriModuleFileSystem } from '../../services/TauriModuleFileSystem';
|
||||
import { PluginSDKRegistry } from '../../services/PluginSDKRegistry';
|
||||
|
||||
export interface EditorServices {
|
||||
uiRegistry: UIRegistry;
|
||||
@@ -104,6 +107,7 @@ export interface EditorServices {
|
||||
propertyRendererRegistry: PropertyRendererRegistry;
|
||||
fieldEditorRegistry: FieldEditorRegistry;
|
||||
buildService: BuildService;
|
||||
userCodeService: UserCodeService;
|
||||
}
|
||||
|
||||
export class ServiceRegistry {
|
||||
@@ -271,6 +275,74 @@ export class ServiceRegistry {
|
||||
console.warn('[ServiceRegistry] Failed to initialize ModuleRegistry:', err);
|
||||
});
|
||||
|
||||
// Initialize UserCodeService for user script compilation and loading
|
||||
// 初始化 UserCodeService 用于用户脚本编译和加载
|
||||
const userCodeService = new UserCodeService(fileSystem);
|
||||
Core.services.registerInstance(UserCodeService, userCodeService);
|
||||
|
||||
// Helper function to compile and load user scripts
|
||||
// 辅助函数:编译和加载用户脚本
|
||||
let currentProjectPath: string | null = null;
|
||||
|
||||
const compileAndLoadUserScripts = async (projectPath: string) => {
|
||||
// Ensure PluginSDKRegistry is initialized before loading user code
|
||||
// 确保在加载用户代码之前 PluginSDKRegistry 已初始化
|
||||
PluginSDKRegistry.initialize();
|
||||
|
||||
try {
|
||||
// Compile runtime scripts | 编译运行时脚本
|
||||
const compileResult = await userCodeService.compile({
|
||||
projectPath: projectPath,
|
||||
target: UserCodeTarget.Runtime
|
||||
});
|
||||
|
||||
if (compileResult.success && compileResult.outputPath) {
|
||||
// Load compiled module | 加载编译后的模块
|
||||
const module = await userCodeService.load(compileResult.outputPath, UserCodeTarget.Runtime);
|
||||
|
||||
// Register user components to editor | 注册用户组件到编辑器
|
||||
userCodeService.registerComponents(module, componentRegistry);
|
||||
|
||||
// Notify that user code has been reloaded | 通知用户代码已重新加载
|
||||
messageHub.publish('usercode:reloaded', {
|
||||
projectPath,
|
||||
exports: Object.keys(module.exports)
|
||||
});
|
||||
} else if (compileResult.errors.length > 0) {
|
||||
console.warn('[UserCodeService] Compilation errors:', compileResult.errors);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[UserCodeService] Failed to compile/load:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Subscribe to project:opened to compile and load user scripts
|
||||
// 订阅 project:opened 以编译和加载用户脚本
|
||||
messageHub.subscribe('project:opened', async (data: { path: string; type: string; name: string }) => {
|
||||
currentProjectPath = data.path;
|
||||
await compileAndLoadUserScripts(data.path);
|
||||
});
|
||||
|
||||
// Subscribe to script file changes (create/delete/modify)
|
||||
// 订阅脚本文件变更(创建/删除/修改)
|
||||
messageHub.subscribe('file:created', async (data: { path: string }) => {
|
||||
if (currentProjectPath && this.isScriptFile(data.path)) {
|
||||
await compileAndLoadUserScripts(currentProjectPath);
|
||||
}
|
||||
});
|
||||
|
||||
messageHub.subscribe('file:deleted', async (data: { path: string }) => {
|
||||
if (currentProjectPath && this.isScriptFile(data.path)) {
|
||||
await compileAndLoadUserScripts(currentProjectPath);
|
||||
}
|
||||
});
|
||||
|
||||
messageHub.subscribe('file:modified', async (data: { path: string }) => {
|
||||
if (currentProjectPath && this.isScriptFile(data.path)) {
|
||||
await compileAndLoadUserScripts(currentProjectPath);
|
||||
}
|
||||
});
|
||||
|
||||
// 注册默认场景模板 - 创建默认相机
|
||||
// Register default scene template - creates default camera
|
||||
this.registerDefaultSceneTemplate();
|
||||
@@ -299,7 +371,8 @@ export class ServiceRegistry {
|
||||
inspectorRegistry,
|
||||
propertyRendererRegistry,
|
||||
fieldEditorRegistry,
|
||||
buildService
|
||||
buildService,
|
||||
userCodeService
|
||||
};
|
||||
}
|
||||
|
||||
@@ -310,6 +383,37 @@ export class ServiceRegistry {
|
||||
}) as EventListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file path is a TypeScript script file (not in editor folder)
|
||||
* 检查文件路径是否为 TypeScript 脚本文件(不在 editor 文件夹中)
|
||||
*/
|
||||
private isScriptFile(filePath: string): boolean {
|
||||
// Must be .ts file | 必须是 .ts 文件
|
||||
if (!filePath.endsWith('.ts')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Normalize path separators | 规范化路径分隔符
|
||||
const normalizedPath = filePath.replace(/\\/g, '/');
|
||||
|
||||
// Must be in scripts folder | 必须在 scripts 文件夹中
|
||||
if (!normalizedPath.includes('/scripts/')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Exclude editor scripts | 排除编辑器脚本
|
||||
if (normalizedPath.includes('/scripts/editor/')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Exclude .esengine folder | 排除 .esengine 文件夹
|
||||
if (normalizedPath.includes('/.esengine/')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册默认场景模板
|
||||
* Register default scene template with default entities
|
||||
|
||||
@@ -125,6 +125,9 @@ export function ContentBrowser({
|
||||
const messageHub = Core.services.resolve(MessageHub);
|
||||
const fileActionRegistry = Core.services.resolve(FileActionRegistry);
|
||||
|
||||
// Refs
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// State
|
||||
const [currentPath, setCurrentPath] = useState<string | null>(null);
|
||||
const [selectedPaths, setSelectedPaths] = useState<Set<string>>(new Set());
|
||||
@@ -329,6 +332,53 @@ export function ${name.charAt(0).toLowerCase() + name.slice(1)}(): void {
|
||||
};
|
||||
}, [fileActionRegistry]);
|
||||
|
||||
// 键盘快捷键处理 | Keyboard shortcuts handling
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
// 如果正在输入或有对话框打开,不处理快捷键
|
||||
// Skip shortcuts if typing or dialog is open
|
||||
if (
|
||||
e.target instanceof HTMLInputElement ||
|
||||
e.target instanceof HTMLTextAreaElement ||
|
||||
renameDialog ||
|
||||
deleteConfirmDialog ||
|
||||
createFileDialog
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 只在内容浏览器区域处理快捷键
|
||||
// Only handle shortcuts when content browser has focus
|
||||
if (!containerRef.current?.contains(document.activeElement) &&
|
||||
document.activeElement !== containerRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
// F2 - 重命名 | Rename
|
||||
if (e.key === 'F2' && selectedPaths.size === 1) {
|
||||
e.preventDefault();
|
||||
const selectedPath = Array.from(selectedPaths)[0];
|
||||
const asset = assets.find(a => a.path === selectedPath);
|
||||
if (asset) {
|
||||
setRenameDialog({ asset, newName: asset.name });
|
||||
}
|
||||
}
|
||||
|
||||
// Delete - 删除 | Delete
|
||||
if (e.key === 'Delete' && selectedPaths.size === 1) {
|
||||
e.preventDefault();
|
||||
const selectedPath = Array.from(selectedPaths)[0];
|
||||
const asset = assets.find(a => a.path === selectedPath);
|
||||
if (asset) {
|
||||
setDeleteConfirmDialog(asset);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
return () => document.removeEventListener('keydown', handleKeyDown);
|
||||
}, [selectedPaths, assets, renameDialog, deleteConfirmDialog, createFileDialog]);
|
||||
|
||||
const getTemplateLabel = (label: string): string => {
|
||||
const mapping = templateLabels[label];
|
||||
if (mapping) {
|
||||
@@ -510,6 +560,9 @@ export function ${name.charAt(0).toLowerCase() + name.slice(1)}(): void {
|
||||
|
||||
// Handle asset click
|
||||
const handleAssetClick = useCallback((asset: AssetItem, e: React.MouseEvent) => {
|
||||
// 聚焦容器以启用键盘快捷键 | Focus container to enable keyboard shortcuts
|
||||
containerRef.current?.focus();
|
||||
|
||||
if (e.shiftKey && lastSelectedPath) {
|
||||
const lastIndex = assets.findIndex(a => a.path === lastSelectedPath);
|
||||
const currentIndex = assets.findIndex(a => a.path === asset.path);
|
||||
@@ -562,9 +615,12 @@ export function ${name.charAt(0).toLowerCase() + name.slice(1)}(): void {
|
||||
const settings = SettingsService.getInstance();
|
||||
const editorCommand = settings.getScriptEditorCommand();
|
||||
|
||||
if (editorCommand && projectPath) {
|
||||
if (editorCommand) {
|
||||
// 使用项目路径,如果没有则使用文件所在目录
|
||||
// Use project path, or file's parent directory if not available
|
||||
const workingDir = projectPath || asset.path.substring(0, asset.path.lastIndexOf('\\')) || asset.path.substring(0, asset.path.lastIndexOf('/'));
|
||||
try {
|
||||
await TauriAPI.openWithEditor(projectPath, editorCommand, asset.path);
|
||||
await TauriAPI.openWithEditor(workingDir, editorCommand, asset.path);
|
||||
return;
|
||||
} catch (error) {
|
||||
console.error('Failed to open with editor:', error);
|
||||
@@ -624,21 +680,38 @@ export function ${name.charAt(0).toLowerCase() + name.slice(1)}(): void {
|
||||
// Handle delete
|
||||
const handleDelete = useCallback(async (asset: AssetItem) => {
|
||||
try {
|
||||
const deletedPath = asset.path;
|
||||
|
||||
if (asset.type === 'folder') {
|
||||
await TauriAPI.deleteFolder(asset.path);
|
||||
// Also delete folder meta file if exists | 同时删除文件夹的 meta 文件
|
||||
try {
|
||||
await TauriAPI.deleteFile(`${asset.path}.meta`);
|
||||
} catch {
|
||||
// Meta file may not exist, ignore | meta 文件可能不存在,忽略
|
||||
}
|
||||
} else {
|
||||
await TauriAPI.deleteFile(asset.path);
|
||||
// Also delete corresponding meta file if exists | 同时删除对应的 meta 文件
|
||||
try {
|
||||
await TauriAPI.deleteFile(`${asset.path}.meta`);
|
||||
} catch {
|
||||
// Meta file may not exist, ignore | meta 文件可能不存在,忽略
|
||||
}
|
||||
}
|
||||
|
||||
if (currentPath) {
|
||||
await loadAssets(currentPath);
|
||||
}
|
||||
|
||||
// Notify that a file was deleted | 通知文件已删除
|
||||
messageHub?.publish('file:deleted', { path: deletedPath });
|
||||
|
||||
setDeleteConfirmDialog(null);
|
||||
} catch (error) {
|
||||
console.error('Failed to delete:', error);
|
||||
}
|
||||
}, [currentPath, loadAssets]);
|
||||
}, [currentPath, loadAssets, messageHub]);
|
||||
|
||||
// Get breadcrumbs
|
||||
const getBreadcrumbs = useCallback(() => {
|
||||
@@ -996,7 +1069,11 @@ export function ${name.charAt(0).toLowerCase() + name.slice(1)}(): void {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`content-browser ${isDrawer ? 'is-drawer' : ''}`}>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={`content-browser ${isDrawer ? 'is-drawer' : ''}`}
|
||||
tabIndex={-1}
|
||||
>
|
||||
{/* Left Panel - Folder Tree */}
|
||||
<div className="content-browser-left">
|
||||
{/* Favorites Section */}
|
||||
@@ -1291,6 +1368,9 @@ export function ${name.charAt(0).toLowerCase() + name.slice(1)}(): void {
|
||||
if (currentPath) {
|
||||
await loadAssets(currentPath);
|
||||
}
|
||||
|
||||
// Notify that a file was created | 通知文件已创建
|
||||
messageHub?.publish('file:created', { path: filePath });
|
||||
} catch (error) {
|
||||
console.error('Failed to create file:', error);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { TauriAPI, DirectoryEntry } from '../api/tauri';
|
||||
import { MessageHub, FileActionRegistry } from '@esengine/editor-core';
|
||||
import { SettingsService } from '../services/SettingsService';
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
import { ContextMenu, ContextMenuItem } from './ContextMenu';
|
||||
import { ConfirmDialog } from './ConfirmDialog';
|
||||
@@ -900,6 +901,27 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
|
||||
return;
|
||||
}
|
||||
|
||||
// 脚本文件使用配置的编辑器打开
|
||||
// Open script files with configured editor
|
||||
if (ext === 'ts' || ext === 'tsx' || ext === 'js' || ext === 'jsx') {
|
||||
const settings = SettingsService.getInstance();
|
||||
const editorCommand = settings.getScriptEditorCommand();
|
||||
|
||||
if (editorCommand) {
|
||||
// 使用项目路径,如果没有则使用文件所在目录
|
||||
// Use project path, or file's parent directory if not available
|
||||
const workingDir = rootPath || node.path.substring(0, node.path.lastIndexOf('\\')) || node.path.substring(0, node.path.lastIndexOf('/'));
|
||||
try {
|
||||
await TauriAPI.openWithEditor(workingDir, editorCommand, node.path);
|
||||
return;
|
||||
} catch (error) {
|
||||
console.error('Failed to open with editor:', error);
|
||||
// 如果失败,回退到系统默认应用
|
||||
// Fall back to system default app if failed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fileActionRegistry) {
|
||||
const handled = await fileActionRegistry.handleDoubleClick(node.path);
|
||||
if (handled) {
|
||||
|
||||
@@ -310,7 +310,12 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
|
||||
className="startup-dialog-btn danger"
|
||||
onClick={async () => {
|
||||
if (deleteConfirm && onDeleteProject) {
|
||||
await onDeleteProject(deleteConfirm);
|
||||
try {
|
||||
await onDeleteProject(deleteConfirm);
|
||||
} catch (error) {
|
||||
console.error('[StartupPage] Failed to delete project:', error);
|
||||
// Error will be handled by App.tsx error dialog
|
||||
}
|
||||
}
|
||||
setDeleteConfirm(null);
|
||||
}}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
background: #1e1e1e;
|
||||
color: #e0e0e0;
|
||||
font-size: 12px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.content-browser.is-drawer {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* Uses .meta files to persistently store each asset's GUID.
|
||||
*/
|
||||
|
||||
import { Core, createLogger } from '@esengine/ecs-framework';
|
||||
import { Core, createLogger, PlatformDetector } from '@esengine/ecs-framework';
|
||||
import { MessageHub } from './MessageHub';
|
||||
import {
|
||||
AssetMetaManager,
|
||||
@@ -215,6 +215,9 @@ export class AssetRegistryService {
|
||||
/** Asset meta manager for .meta file management */
|
||||
private _metaManager: AssetMetaManager;
|
||||
|
||||
/** Tauri event unlisten function | Tauri 事件取消监听函数 */
|
||||
private _eventUnlisten: (() => void) | undefined;
|
||||
|
||||
/** Manifest file name */
|
||||
static readonly MANIFEST_FILE = 'asset-manifest.json';
|
||||
/** Current manifest version */
|
||||
@@ -311,6 +314,10 @@ export class AssetRegistryService {
|
||||
// Save updated manifest
|
||||
await this._saveManifest();
|
||||
|
||||
// Subscribe to file change events (Tauri only)
|
||||
// 订阅文件变化事件(仅 Tauri 环境)
|
||||
await this._subscribeToFileChanges();
|
||||
|
||||
logger.info(`Project assets loaded: ${this._database.getStatistics().totalAssets} assets`);
|
||||
|
||||
// Publish event
|
||||
@@ -320,10 +327,77 @@ export class AssetRegistryService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to file change events from Tauri backend
|
||||
* 订阅来自 Tauri 后端的文件变化事件
|
||||
*/
|
||||
private async _subscribeToFileChanges(): Promise<void> {
|
||||
// Only in Tauri environment
|
||||
// 仅在 Tauri 环境中
|
||||
if (!PlatformDetector.isTauriEnvironment()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { listen } = await import('@tauri-apps/api/event');
|
||||
|
||||
// Listen to user-code:file-changed event
|
||||
// 监听 user-code:file-changed 事件
|
||||
this._eventUnlisten = await listen<{
|
||||
changeType: string;
|
||||
paths: string[];
|
||||
}>('user-code:file-changed', async (event) => {
|
||||
const { changeType, paths } = event.payload;
|
||||
|
||||
logger.debug('File change event received | 收到文件变化事件', { changeType, paths });
|
||||
|
||||
// Handle file creation - register new assets and generate .meta
|
||||
// 处理文件创建 - 注册新资产并生成 .meta
|
||||
if (changeType === 'create' || changeType === 'modify') {
|
||||
for (const absolutePath of paths) {
|
||||
// Skip .meta files
|
||||
if (absolutePath.endsWith('.meta')) continue;
|
||||
|
||||
// Register or refresh the asset
|
||||
await this.registerAsset(absolutePath);
|
||||
}
|
||||
} else if (changeType === 'remove') {
|
||||
for (const absolutePath of paths) {
|
||||
// Skip .meta files
|
||||
if (absolutePath.endsWith('.meta')) continue;
|
||||
|
||||
// Unregister the asset
|
||||
await this.unregisterAsset(absolutePath);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
logger.info('Subscribed to file change events | 已订阅文件变化事件');
|
||||
} catch (error) {
|
||||
logger.warn('Failed to subscribe to file change events | 订阅文件变化事件失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from file change events
|
||||
* 取消订阅文件变化事件
|
||||
*/
|
||||
private _unsubscribeFromFileChanges(): void {
|
||||
if (this._eventUnlisten) {
|
||||
this._eventUnlisten();
|
||||
this._eventUnlisten = undefined;
|
||||
logger.debug('Unsubscribed from file change events | 已取消订阅文件变化事件');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unload current project
|
||||
*/
|
||||
unloadProject(): void {
|
||||
// Unsubscribe from file change events
|
||||
// 取消订阅文件变化事件
|
||||
this._unsubscribeFromFileChanges();
|
||||
|
||||
this._projectPath = null;
|
||||
this._manifest = null;
|
||||
this._database.clear();
|
||||
|
||||
@@ -130,13 +130,10 @@ export class ProjectService implements IService {
|
||||
const assetsPath = `${projectPath}${sep}assets`;
|
||||
await this.fileAPI.createDirectory(assetsPath);
|
||||
|
||||
// Create types folder for type definitions
|
||||
// 创建类型定义文件夹
|
||||
const typesPath = `${projectPath}${sep}types`;
|
||||
await this.fileAPI.createDirectory(typesPath);
|
||||
|
||||
// Create tsconfig.json for TypeScript support
|
||||
// 创建 tsconfig.json 用于 TypeScript 支持
|
||||
// Create tsconfig.json for runtime scripts (components, systems)
|
||||
// 创建运行时脚本的 tsconfig.json(组件、系统等)
|
||||
// Note: paths will be populated by update_project_tsconfig when project is opened
|
||||
// 注意:paths 会在项目打开时由 update_project_tsconfig 填充
|
||||
const tsConfig = {
|
||||
compilerOptions: {
|
||||
target: 'ES2020',
|
||||
@@ -149,21 +146,40 @@ export class ProjectService implements IService {
|
||||
forceConsistentCasingInFileNames: true,
|
||||
experimentalDecorators: true,
|
||||
emitDecoratorMetadata: true,
|
||||
noEmit: true,
|
||||
// Reference local type definitions
|
||||
// 引用本地类型定义文件
|
||||
typeRoots: ['./types'],
|
||||
paths: {
|
||||
'@esengine/ecs-framework': ['./types/ecs-framework.d.ts'],
|
||||
'@esengine/engine-core': ['./types/engine-core.d.ts']
|
||||
}
|
||||
noEmit: true
|
||||
// paths will be added by editor when project is opened
|
||||
// paths 会在编辑器打开项目时添加
|
||||
},
|
||||
include: ['scripts/**/*.ts'],
|
||||
exclude: ['.esengine']
|
||||
exclude: ['scripts/editor/**/*.ts', '.esengine']
|
||||
};
|
||||
const tsConfigPath = `${projectPath}${sep}tsconfig.json`;
|
||||
await this.fileAPI.writeFileContent(tsConfigPath, JSON.stringify(tsConfig, null, 2));
|
||||
|
||||
// Create tsconfig.editor.json for editor extension scripts
|
||||
// 创建编辑器扩展脚本的 tsconfig.editor.json
|
||||
const tsConfigEditor = {
|
||||
compilerOptions: {
|
||||
target: 'ES2020',
|
||||
module: 'ESNext',
|
||||
moduleResolution: 'bundler',
|
||||
lib: ['ES2020', 'DOM'],
|
||||
strict: true,
|
||||
esModuleInterop: true,
|
||||
skipLibCheck: true,
|
||||
forceConsistentCasingInFileNames: true,
|
||||
experimentalDecorators: true,
|
||||
emitDecoratorMetadata: true,
|
||||
noEmit: true
|
||||
// paths will be added by editor when project is opened
|
||||
// paths 会在编辑器打开项目时添加
|
||||
},
|
||||
include: ['scripts/editor/**/*.ts'],
|
||||
exclude: ['.esengine']
|
||||
};
|
||||
const tsConfigEditorPath = `${projectPath}${sep}tsconfig.editor.json`;
|
||||
await this.fileAPI.writeFileContent(tsConfigEditorPath, JSON.stringify(tsConfigEditor, null, 2));
|
||||
|
||||
await this.messageHub.publish('project:created', {
|
||||
path: projectPath
|
||||
});
|
||||
|
||||
@@ -215,8 +215,9 @@ export interface IUserCodeService {
|
||||
* - Classes extending System
|
||||
*
|
||||
* @param module - User code module | 用户代码模块
|
||||
* @param componentRegistry - Optional ComponentRegistry to register components | 可选的 ComponentRegistry 用于注册组件
|
||||
*/
|
||||
registerComponents(module: UserCodeModule): void;
|
||||
registerComponents(module: UserCodeModule, componentRegistry?: any): void;
|
||||
|
||||
/**
|
||||
* Register editor extensions from user module.
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import type { IService } from '@esengine/ecs-framework';
|
||||
import { Injectable, createLogger } from '@esengine/ecs-framework';
|
||||
import { Injectable, createLogger, PlatformDetector } from '@esengine/ecs-framework';
|
||||
import type {
|
||||
IUserCodeService,
|
||||
UserScriptInfo,
|
||||
@@ -110,6 +110,9 @@ export class UserCodeService implements IService, IUserCodeService {
|
||||
const errors: CompileError[] = [];
|
||||
const warnings: CompileError[] = [];
|
||||
|
||||
// Store project path for later use in load() | 存储项目路径供 load() 使用
|
||||
this._currentProjectPath = options.projectPath;
|
||||
|
||||
const sep = options.projectPath.includes('\\') ? '\\' : '/';
|
||||
const scriptsDir = `${options.projectPath}${sep}${SCRIPTS_DIR}`;
|
||||
const outputDir = options.outputDir || `${options.projectPath}${sep}${USER_CODE_OUTPUT_DIR}`;
|
||||
@@ -146,14 +149,35 @@ export class UserCodeService implements IService, IUserCodeService {
|
||||
const entryPath = `${outputDir}${sep}_entry_${options.target}.ts`;
|
||||
await this._fileSystem.writeFile(entryPath, entryContent);
|
||||
|
||||
// Create shim files for framework dependencies | 创建框架依赖的 shim 文件
|
||||
await this._createDependencyShims(outputDir, options.target);
|
||||
|
||||
// Determine global name for IIFE output | 确定 IIFE 输出的全局名称
|
||||
const globalName = options.target === UserCodeTarget.Runtime
|
||||
? '__USER_RUNTIME_EXPORTS__'
|
||||
: '__USER_EDITOR_EXPORTS__';
|
||||
|
||||
// Build alias map for framework dependencies | 构建框架依赖的别名映射
|
||||
const shimPath = `${outputDir}${sep}_shim_ecs_framework.js`.replace(/\\/g, '/');
|
||||
const alias: Record<string, string> = {
|
||||
'@esengine/ecs-framework': shimPath,
|
||||
'@esengine/core': shimPath,
|
||||
'@esengine/engine-core': shimPath,
|
||||
'@esengine/math': shimPath
|
||||
};
|
||||
|
||||
// Compile using esbuild (via Tauri command or direct) | 使用 esbuild 编译
|
||||
// Use IIFE format to avoid ES module import issues in Tauri
|
||||
// 使用 IIFE 格式以避免 Tauri 中的 ES 模块导入问题
|
||||
const compileResult = await this._runEsbuild({
|
||||
entryPath,
|
||||
outputPath,
|
||||
format: options.format || 'esm',
|
||||
format: 'iife', // Always use IIFE for Tauri compatibility | 始终使用 IIFE 以兼容 Tauri
|
||||
globalName,
|
||||
sourceMap: options.sourceMap ?? true,
|
||||
minify: options.minify ?? false,
|
||||
external: this._getExternalDependencies(options.target),
|
||||
external: [], // Don't use external, use alias instead | 不使用 external,使用 alias
|
||||
alias,
|
||||
projectRoot: options.projectPath
|
||||
});
|
||||
|
||||
@@ -207,12 +231,30 @@ export class UserCodeService implements IService, IUserCodeService {
|
||||
*/
|
||||
async load(modulePath: string, target: UserCodeTarget): Promise<UserCodeModule> {
|
||||
try {
|
||||
// Add cache-busting query parameter for hot reload | 添加缓存破坏参数用于热更新
|
||||
const cacheBuster = `?t=${Date.now()}`;
|
||||
const moduleUrl = `file://${modulePath}${cacheBuster}`;
|
||||
let moduleExports: Record<string, any>;
|
||||
|
||||
// Dynamic import the module | 动态导入模块
|
||||
const moduleExports = await import(/* @vite-ignore */ moduleUrl);
|
||||
if (PlatformDetector.isTauriEnvironment()) {
|
||||
// In Tauri, read file content and execute via script tag
|
||||
// 在 Tauri 中,读取文件内容并通过 script 标签执行
|
||||
// This avoids CORS and module resolution issues
|
||||
// 这避免了 CORS 和模块解析问题
|
||||
const { invoke } = await import('@tauri-apps/api/core');
|
||||
|
||||
const content = await invoke<string>('read_file_content', {
|
||||
path: modulePath
|
||||
});
|
||||
|
||||
logger.debug(`Loading module via script injection`, { originalPath: modulePath });
|
||||
|
||||
// Execute module code and capture exports | 执行模块代码并捕获导出
|
||||
moduleExports = await this._executeModuleCode(content, target);
|
||||
} else {
|
||||
// Fallback to file:// for non-Tauri environments
|
||||
// 非 Tauri 环境使用 file://
|
||||
const cacheBuster = `?t=${Date.now()}`;
|
||||
const moduleUrl = `file://${modulePath}${cacheBuster}`;
|
||||
moduleExports = await import(/* @vite-ignore */ moduleUrl);
|
||||
}
|
||||
|
||||
const module: UserCodeModule = {
|
||||
id: `user-${target}-${Date.now()}`,
|
||||
@@ -273,7 +315,7 @@ export class UserCodeService implements IService, IUserCodeService {
|
||||
*
|
||||
* @param module - User code module | 用户代码模块
|
||||
*/
|
||||
registerComponents(module: UserCodeModule): void {
|
||||
registerComponents(module: UserCodeModule, componentRegistry?: any): void {
|
||||
if (module.target !== UserCodeTarget.Runtime) {
|
||||
logger.warn('Cannot register components from editor module | 无法从编辑器模块注册组件');
|
||||
return;
|
||||
@@ -289,10 +331,24 @@ export class UserCodeService implements IService, IUserCodeService {
|
||||
|
||||
// Check if it's a Component subclass | 检查是否是 Component 子类
|
||||
if (this._isComponentClass(exported)) {
|
||||
// Register with ComponentRegistry | 注册到 ComponentRegistry
|
||||
// Note: Actual registration depends on runtime context
|
||||
// 注意:实际注册取决于运行时上下文
|
||||
logger.debug(`Found component: ${name} | 发现组件: ${name}`);
|
||||
|
||||
// Register with ComponentRegistry if provided | 如果提供了 ComponentRegistry 则注册
|
||||
// ComponentRegistry expects ComponentTypeInfo object, not the class directly
|
||||
// ComponentRegistry 期望 ComponentTypeInfo 对象,而不是直接传入类
|
||||
if (componentRegistry && typeof componentRegistry.register === 'function') {
|
||||
try {
|
||||
componentRegistry.register({
|
||||
name: name,
|
||||
type: exported,
|
||||
category: 'User', // User-defined components | 用户自定义组件
|
||||
description: `User component: ${name}`
|
||||
});
|
||||
} catch (err) {
|
||||
logger.warn(`Failed to register component ${name} | 注册组件 ${name} 失败:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
componentCount++;
|
||||
}
|
||||
|
||||
@@ -373,7 +429,7 @@ export class UserCodeService implements IService, IUserCodeService {
|
||||
|
||||
try {
|
||||
// Check if we're in Tauri environment | 检查是否在 Tauri 环境
|
||||
if (typeof window !== 'undefined' && '__TAURI__' in window) {
|
||||
if (PlatformDetector.isTauriEnvironment()) {
|
||||
const { invoke } = await import('@tauri-apps/api/core');
|
||||
const { listen } = await import('@tauri-apps/api/event');
|
||||
|
||||
@@ -461,7 +517,7 @@ export class UserCodeService implements IService, IUserCodeService {
|
||||
}
|
||||
|
||||
// Stop backend file watcher | 停止后端文件监视器
|
||||
if (typeof window !== 'undefined' && '__TAURI__' in window) {
|
||||
if (PlatformDetector.isTauriEnvironment()) {
|
||||
const { invoke } = await import('@tauri-apps/api/core');
|
||||
await invoke('stop_watch_scripts', {
|
||||
projectPath: this._currentProjectPath
|
||||
@@ -577,6 +633,17 @@ export class UserCodeService implements IService, IUserCodeService {
|
||||
/**
|
||||
* Build entry point content that re-exports all user scripts.
|
||||
* 构建重新导出所有用户脚本的入口点内容。
|
||||
*
|
||||
* Entry file is in: {projectPath}/.esengine/compiled/_entry_runtime.ts
|
||||
* Scripts are in: {projectPath}/scripts/
|
||||
* So the relative path from entry to scripts is: ../../scripts/
|
||||
*
|
||||
* For IIFE format, we inject shims that map global variables to module imports.
|
||||
* This allows user code to use `import { Component } from '@esengine/ecs-framework'`
|
||||
* while actually accessing `window.__ESENGINE_FRAMEWORK__`.
|
||||
* 对于 IIFE 格式,我们注入 shim 将全局变量映射到模块导入。
|
||||
* 这使用户代码可以使用 `import { Component } from '@esengine/ecs-framework'`,
|
||||
* 实际上访问的是 `window.__ESENGINE_FRAMEWORK__`。
|
||||
*/
|
||||
private _buildEntryPoint(
|
||||
scripts: UserScriptInfo[],
|
||||
@@ -589,22 +656,60 @@ export class UserCodeService implements IService, IUserCodeService {
|
||||
''
|
||||
];
|
||||
|
||||
// Entry file is in .esengine/compiled/, need to go up 2 levels to reach project root
|
||||
// 入口文件在 .esengine/compiled/ 目录,需要上升 2 级到达项目根目录
|
||||
const relativePrefix = `../../${SCRIPTS_DIR}`;
|
||||
|
||||
for (const script of scripts) {
|
||||
// Convert absolute path to relative import | 将绝对路径转换为相对导入
|
||||
const relativePath = script.relativePath.replace(/\\/g, '/').replace(/\.tsx?$/, '');
|
||||
|
||||
if (script.exports.length > 0) {
|
||||
lines.push(`export { ${script.exports.join(', ')} } from './${SCRIPTS_DIR}/${relativePath}';`);
|
||||
lines.push(`export { ${script.exports.join(', ')} } from '${relativePrefix}/${relativePath}';`);
|
||||
} else {
|
||||
// Re-export everything if we couldn't detect specific exports
|
||||
// 如果无法检测到具体导出,则重新导出所有内容
|
||||
lines.push(`export * from './${SCRIPTS_DIR}/${relativePath}';`);
|
||||
lines.push(`export * from '${relativePrefix}/${relativePath}';`);
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create shim files that map global variables to module imports.
|
||||
* 创建将全局变量映射到模块导入的 shim 文件。
|
||||
*
|
||||
* This is used for IIFE format to resolve external dependencies.
|
||||
* The shim exports the global __ESENGINE__.ecsFramework which is set by PluginSDKRegistry.
|
||||
* 这用于 IIFE 格式解析外部依赖。
|
||||
* shim 导出全局的 __ESENGINE__.ecsFramework,由 PluginSDKRegistry 设置。
|
||||
*
|
||||
* @param outputDir - Output directory | 输出目录
|
||||
* @param target - Target environment | 目标环境
|
||||
* @returns Array of shim file paths | shim 文件路径数组
|
||||
*/
|
||||
private async _createDependencyShims(
|
||||
outputDir: string,
|
||||
target: UserCodeTarget
|
||||
): Promise<string[]> {
|
||||
const sep = outputDir.includes('\\') ? '\\' : '/';
|
||||
const shimPaths: string[] = [];
|
||||
|
||||
// Create shim for @esengine/ecs-framework | 为 @esengine/ecs-framework 创建 shim
|
||||
// This uses window.__ESENGINE__.ecsFramework set by PluginSDKRegistry
|
||||
// 这使用 PluginSDKRegistry 设置的 window.__ESENGINE__.ecsFramework
|
||||
const ecsShimPath = `${outputDir}${sep}_shim_ecs_framework.js`;
|
||||
const ecsShimContent = `// Shim for @esengine/ecs-framework
|
||||
// Maps to window.__ESENGINE__.ecsFramework set by PluginSDKRegistry
|
||||
module.exports = (typeof window !== 'undefined' && window.__ESENGINE__ && window.__ESENGINE__.ecsFramework) || {};
|
||||
`;
|
||||
await this._fileSystem.writeFile(ecsShimPath, ecsShimContent);
|
||||
shimPaths.push(ecsShimPath);
|
||||
|
||||
return shimPaths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get external dependencies that should not be bundled.
|
||||
* 获取不应打包的外部依赖。
|
||||
@@ -640,14 +745,16 @@ export class UserCodeService implements IService, IUserCodeService {
|
||||
entryPath: string;
|
||||
outputPath: string;
|
||||
format: 'esm' | 'iife';
|
||||
globalName?: string;
|
||||
sourceMap: boolean;
|
||||
minify: boolean;
|
||||
external: string[];
|
||||
alias?: Record<string, string>;
|
||||
projectRoot: string;
|
||||
}): Promise<{ success: boolean; errors: CompileError[] }> {
|
||||
try {
|
||||
// Check if we're in Tauri environment | 检查是否在 Tauri 环境
|
||||
if (typeof window !== 'undefined' && '__TAURI__' in window) {
|
||||
if (PlatformDetector.isTauriEnvironment()) {
|
||||
// Use Tauri command | 使用 Tauri 命令
|
||||
const { invoke } = await import('@tauri-apps/api/core');
|
||||
|
||||
@@ -665,9 +772,11 @@ export class UserCodeService implements IService, IUserCodeService {
|
||||
entryPath: options.entryPath,
|
||||
outputPath: options.outputPath,
|
||||
format: options.format,
|
||||
globalName: options.globalName,
|
||||
sourceMap: options.sourceMap,
|
||||
minify: options.minify,
|
||||
external: options.external,
|
||||
alias: options.alias,
|
||||
projectRoot: options.projectRoot
|
||||
}
|
||||
});
|
||||
@@ -701,6 +810,56 @@ export class UserCodeService implements IService, IUserCodeService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute compiled module code and return exports.
|
||||
* 执行编译后的模块代码并返回导出。
|
||||
*
|
||||
* The code should be in IIFE format that sets a global variable.
|
||||
* 代码应该是设置全局变量的 IIFE 格式。
|
||||
*
|
||||
* @param code - Compiled JavaScript code | 编译后的 JavaScript 代码
|
||||
* @param target - Target environment | 目标环境
|
||||
* @returns Module exports | 模块导出
|
||||
*/
|
||||
private async _executeModuleCode(
|
||||
code: string,
|
||||
target: UserCodeTarget
|
||||
): Promise<Record<string, any>> {
|
||||
// Determine global name based on target | 根据目标确定全局名称
|
||||
const globalName = target === UserCodeTarget.Runtime
|
||||
? '__USER_RUNTIME_EXPORTS__'
|
||||
: '__USER_EDITOR_EXPORTS__';
|
||||
|
||||
// Clear any previous exports | 清除之前的导出
|
||||
(window as any)[globalName] = undefined;
|
||||
|
||||
try {
|
||||
// esbuild generates: var __USER_RUNTIME_EXPORTS__ = (() => {...})();
|
||||
// When executed via new Function(), var declarations stay in function scope
|
||||
// We need to replace "var globalName" with "window.globalName" to expose it
|
||||
// esbuild 生成: var __USER_RUNTIME_EXPORTS__ = (() => {...})();
|
||||
// 通过 new Function() 执行时,var 声明在函数作用域内
|
||||
// 需要替换 "var globalName" 为 "window.globalName" 以暴露到全局
|
||||
const modifiedCode = code.replace(
|
||||
new RegExp(`^"use strict";\\s*var ${globalName}`, 'm'),
|
||||
`"use strict";\nwindow.${globalName}`
|
||||
);
|
||||
|
||||
// Execute the IIFE code | 执行 IIFE 代码
|
||||
// eslint-disable-next-line no-new-func
|
||||
const executeScript = new Function(modifiedCode);
|
||||
executeScript();
|
||||
|
||||
// Get exports from global | 从全局获取导出
|
||||
const exports = (window as any)[globalName] || {};
|
||||
|
||||
return exports;
|
||||
} catch (error) {
|
||||
logger.error('Failed to execute user code | 执行用户代码失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure directory exists, create if not.
|
||||
* 确保目录存在,如果不存在则创建。
|
||||
@@ -719,17 +878,29 @@ export class UserCodeService implements IService, IUserCodeService {
|
||||
/**
|
||||
* Check if a class extends Component.
|
||||
* 检查类是否继承自 Component。
|
||||
*
|
||||
* Uses the actual Component class from the global framework to check inheritance.
|
||||
* 使用全局框架中的实际 Component 类来检查继承关系。
|
||||
*/
|
||||
private _isComponentClass(cls: any): boolean {
|
||||
// Check prototype chain for Component | 检查原型链中是否有 Component
|
||||
let proto = cls.prototype;
|
||||
while (proto) {
|
||||
if (proto.constructor?.name === 'Component') {
|
||||
return true;
|
||||
}
|
||||
proto = Object.getPrototypeOf(proto);
|
||||
// Get Component class from global framework | 从全局框架获取 Component 类
|
||||
const framework = (window as any).__ESENGINE__?.ecsFramework;
|
||||
|
||||
if (!framework?.Component) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use instanceof or prototype chain check | 使用 instanceof 或原型链检查
|
||||
try {
|
||||
const ComponentClass = framework.Component;
|
||||
|
||||
// Check if cls.prototype is an instance of Component
|
||||
// 检查 cls.prototype 是否是 Component 的实例
|
||||
return cls.prototype instanceof ComponentClass ||
|
||||
ComponentClass.prototype.isPrototypeOf(cls.prototype);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -110,7 +110,9 @@ function copyModule(module) {
|
||||
fs.copyFileSync(module.moduleJsonPath, destModuleJson);
|
||||
|
||||
// Copy dist/index.js if exists
|
||||
// 如果存在则拷贝 dist/index.js
|
||||
let hasRuntime = false;
|
||||
let hasTypes = false;
|
||||
let sizeKB = 'N/A';
|
||||
|
||||
if (fs.existsSync(module.distPath)) {
|
||||
@@ -118,6 +120,7 @@ function copyModule(module) {
|
||||
fs.copyFileSync(module.distPath, destIndexJs);
|
||||
|
||||
// Also copy source map if exists
|
||||
// 如果存在则拷贝 source map
|
||||
const sourceMapPath = module.distPath + '.map';
|
||||
if (fs.existsSync(sourceMapPath)) {
|
||||
fs.copyFileSync(sourceMapPath, destIndexJs + '.map');
|
||||
@@ -128,7 +131,16 @@ function copyModule(module) {
|
||||
hasRuntime = true;
|
||||
}
|
||||
|
||||
return { hasRuntime, sizeKB };
|
||||
// Copy type definitions (.d.ts) if exists
|
||||
// 如果存在则拷贝类型定义文件 (.d.ts)
|
||||
const typesPath = module.distPath.replace(/\.js$/, '.d.ts');
|
||||
if (fs.existsSync(typesPath)) {
|
||||
const destDts = path.join(moduleOutputDir, 'index.d.ts');
|
||||
fs.copyFileSync(typesPath, destDts);
|
||||
hasTypes = true;
|
||||
}
|
||||
|
||||
return { hasRuntime, hasTypes, sizeKB };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -203,10 +215,11 @@ function main() {
|
||||
const moduleInfos = [];
|
||||
|
||||
for (const module of modules) {
|
||||
const { hasRuntime, sizeKB } = copyModule(module);
|
||||
const { hasRuntime, hasTypes, sizeKB } = copyModule(module);
|
||||
|
||||
if (hasRuntime) {
|
||||
console.log(` ✓ ${module.id} (${sizeKB} KB)`);
|
||||
const typesIndicator = hasTypes ? ' +types' : '';
|
||||
console.log(` ✓ ${module.id} (${sizeKB} KB${typesIndicator})`);
|
||||
} else {
|
||||
console.log(` ○ ${module.id} (config only)`);
|
||||
}
|
||||
@@ -216,6 +229,7 @@ function main() {
|
||||
name: module.name,
|
||||
displayName: module.displayName,
|
||||
hasRuntime,
|
||||
hasTypes,
|
||||
editorPackage: module.editorPackage,
|
||||
isCore: module.isCore,
|
||||
category: module.category
|
||||
|
||||
Reference in New Issue
Block a user