Files
esengine/packages/editor/editor-app/src-tauri/src/commands/plugin.rs
YHH 155411e743 refactor: reorganize package structure and decouple framework packages (#338)
* refactor: reorganize package structure and decouple framework packages

## Package Structure Reorganization
- Reorganized 55 packages into categorized subdirectories:
  - packages/framework/ - Generic framework (Laya/Cocos compatible)
  - packages/engine/ - ESEngine core modules
  - packages/rendering/ - Rendering modules (WASM dependent)
  - packages/physics/ - Physics modules
  - packages/streaming/ - World streaming
  - packages/network-ext/ - Network extensions
  - packages/editor/ - Editor framework and plugins
  - packages/rust/ - Rust WASM engine
  - packages/tools/ - Build tools and SDK

## Framework Package Decoupling
- Decoupled behavior-tree and blueprint packages from ESEngine dependencies
- Created abstracted interfaces (IBTAssetManager, IBehaviorTreeAssetContent)
- ESEngine-specific code moved to esengine/ subpath exports
- Framework packages now usable with Cocos/Laya without ESEngine

## CI Configuration
- Updated CI to only type-check and lint framework packages
- Added type-check:framework and lint:framework scripts

## Breaking Changes
- Package import paths changed due to directory reorganization
- ESEngine integrations now use subpath imports (e.g., '@esengine/behavior-tree/esengine')

* fix: update es-engine file path after directory reorganization

* docs: update README to focus on framework over engine

* ci: only build framework packages, remove Rust/WASM dependencies

* fix: remove esengine subpath from behavior-tree and blueprint builds

ESEngine integration code will only be available in full engine builds.
Framework packages are now purely engine-agnostic.

* fix: move network-protocols to framework, build both in CI

* fix: update workflow paths from packages/core to packages/framework/core

* fix: exclude esengine folder from type-check in behavior-tree and blueprint

* fix: update network tsconfig references to new paths

* fix: add test:ci:framework to only test framework packages in CI

* fix: only build core and math npm packages in CI

* fix: exclude test files from CodeQL and fix string escaping security issue
2025-12-26 14:50:35 +08:00

271 lines
8.0 KiB
Rust

//! Plugin management commands
//!
//! Building, installing, and uninstalling editor plugins.
use std::fs;
use std::io::{Cursor, Write};
use std::path::Path;
use std::process::Command;
use tauri::{AppHandle, Emitter};
use zip::write::FileOptions;
use zip::ZipArchive;
/// Build progress event payload
#[derive(serde::Serialize, Clone)]
pub struct BuildProgress {
pub step: String,
pub output: Option<String>,
}
/// Build a plugin from source
#[tauri::command]
pub async fn build_plugin(plugin_folder: String, app: AppHandle) -> Result<String, String> {
let plugin_path = Path::new(&plugin_folder);
if !plugin_path.exists() {
return Err(format!("Plugin folder does not exist: {}", plugin_folder));
}
let package_json_path = plugin_path.join("package.json");
if !package_json_path.exists() {
return Err("package.json not found in plugin folder".to_string());
}
let build_cache_dir = plugin_path.join(".build-cache");
if !build_cache_dir.exists() {
fs::create_dir_all(&build_cache_dir)
.map_err(|e| format!("Failed to create .build-cache directory: {}", e))?;
}
let pnpm_command = if cfg!(target_os = "windows") {
"pnpm.cmd"
} else {
"pnpm"
};
// Step 1: Install dependencies
app.emit(
"plugin-build-progress",
BuildProgress {
step: "install".to_string(),
output: None,
},
)
.ok();
let install_output = Command::new(&pnpm_command)
.args(["install"])
.current_dir(&plugin_folder)
.output()
.map_err(|e| format!("Failed to run pnpm install: {}", e))?;
if !install_output.status.success() {
return Err(format!(
"pnpm install failed: {}",
String::from_utf8_lossy(&install_output.stderr)
));
}
// Step 2: Build
app.emit(
"plugin-build-progress",
BuildProgress {
step: "build".to_string(),
output: None,
},
)
.ok();
let build_output = Command::new(&pnpm_command)
.args(["run", "build"])
.current_dir(&plugin_folder)
.output()
.map_err(|e| format!("Failed to run pnpm run build: {}", e))?;
if !build_output.status.success() {
return Err(format!(
"pnpm run build failed: {}",
String::from_utf8_lossy(&build_output.stderr)
));
}
let dist_path = plugin_path.join("dist");
if !dist_path.exists() {
return Err("dist directory not found after build".to_string());
}
// Step 3: Package
app.emit(
"plugin-build-progress",
BuildProgress {
step: "package".to_string(),
output: None,
},
)
.ok();
let zip_path = build_cache_dir.join("index.zip");
let zip_file =
fs::File::create(&zip_path).map_err(|e| format!("Failed to create zip file: {}", e))?;
let mut zip = zip::ZipWriter::new(zip_file);
let options = FileOptions::default()
.compression_method(zip::CompressionMethod::Deflated)
.unix_permissions(0o755);
// Add package.json
let package_json_content = fs::read(&package_json_path)
.map_err(|e| format!("Failed to read package.json: {}", e))?;
zip.start_file("package.json", options)
.map_err(|e| format!("Failed to add package.json to zip: {}", e))?;
zip.write_all(&package_json_content)
.map_err(|e| format!("Failed to write package.json to zip: {}", e))?;
// Add dist directory
add_directory_to_zip(&mut zip, plugin_path, &dist_path, options)
.map_err(|e| format!("Failed to add dist directory to zip: {}", e))?;
zip.finish()
.map_err(|e| format!("Failed to finalize zip: {}", e))?;
// Step 4: Complete
app.emit(
"plugin-build-progress",
BuildProgress {
step: "complete".to_string(),
output: None,
},
)
.ok();
Ok(zip_path.to_string_lossy().to_string())
}
fn add_directory_to_zip<W: std::io::Write + std::io::Seek>(
zip: &mut zip::ZipWriter<W>,
base_path: &Path,
current_path: &Path,
options: FileOptions,
) -> Result<(), String> {
let entries = fs::read_dir(current_path)
.map_err(|e| format!("Failed to read directory {}: {}", current_path.display(), e))?;
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
add_directory_to_zip(zip, base_path, &path, options)?;
} else {
let relative_path = path
.strip_prefix(base_path)
.map_err(|e| format!("Failed to get relative path: {}", e))?;
let zip_path = relative_path.to_string_lossy().replace('\\', "/");
let file_content = fs::read(&path)
.map_err(|e| format!("Failed to read file {}: {}", path.display(), e))?;
zip.start_file(&zip_path, options)
.map_err(|e| format!("Failed to add file {} to zip: {}", zip_path, e))?;
zip.write_all(&file_content)
.map_err(|e| format!("Failed to write file {} to zip: {}", zip_path, e))?;
}
}
Ok(())
}
/// Install a plugin from marketplace
#[tauri::command]
pub async fn install_marketplace_plugin(
project_path: String,
plugin_id: String,
zip_data_base64: String,
) -> Result<String, String> {
use base64::{engine::general_purpose, Engine as _};
let project_path = Path::new(&project_path);
if !project_path.exists() {
return Err(format!(
"Project path does not exist: {}",
project_path.display()
));
}
let plugins_dir = project_path.join("plugins");
if !plugins_dir.exists() {
fs::create_dir_all(&plugins_dir)
.map_err(|e| format!("Failed to create plugins directory: {}", e))?;
}
let plugin_dir = plugins_dir.join(&plugin_id);
if plugin_dir.exists() {
fs::remove_dir_all(&plugin_dir)
.map_err(|e| format!("Failed to remove old plugin directory: {}", e))?;
}
fs::create_dir_all(&plugin_dir)
.map_err(|e| format!("Failed to create plugin directory: {}", e))?;
let zip_bytes = general_purpose::STANDARD
.decode(&zip_data_base64)
.map_err(|e| format!("Failed to decode base64 ZIP data: {}", e))?;
let cursor = Cursor::new(zip_bytes);
let mut archive =
ZipArchive::new(cursor).map_err(|e| format!("Failed to read ZIP archive: {}", e))?;
for i in 0..archive.len() {
let mut file = archive
.by_index(i)
.map_err(|e| format!("Failed to read ZIP entry {}: {}", i, e))?;
let file_path = match file.enclosed_name() {
Some(path) => path,
None => continue,
};
let out_path = plugin_dir.join(file_path);
if file.is_dir() {
fs::create_dir_all(&out_path)
.map_err(|e| format!("Failed to create directory {}: {}", out_path.display(), e))?;
} else {
if let Some(parent) = out_path.parent() {
fs::create_dir_all(parent)
.map_err(|e| format!("Failed to create parent directory: {}", e))?;
}
let mut out_file = fs::File::create(&out_path)
.map_err(|e| format!("Failed to create file {}: {}", out_path.display(), e))?;
std::io::copy(&mut file, &mut out_file)
.map_err(|e| format!("Failed to write file {}: {}", out_path.display(), e))?;
}
}
Ok(plugin_dir.to_string_lossy().to_string())
}
/// Uninstall a plugin
#[tauri::command]
pub async fn uninstall_marketplace_plugin(
project_path: String,
plugin_id: String,
) -> Result<(), String> {
let project_path = Path::new(&project_path);
let plugin_dir = project_path.join("plugins").join(&plugin_id);
if !plugin_dir.exists() {
return Err(format!(
"Plugin directory does not exist: {}",
plugin_dir.display()
));
}
fs::remove_dir_all(&plugin_dir)
.map_err(|e| format!("Failed to remove plugin directory: {}", e))?;
Ok(())
}