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:
@@ -1,56 +1,32 @@
|
||||
/**
|
||||
* Runtime Module Resolver
|
||||
* 运行时模块解析器
|
||||
*
|
||||
* Resolves runtime module paths based on environment and configuration
|
||||
* 根据环境和配置解析运行时模块路径
|
||||
*
|
||||
* 运行时文件打包在编辑器内,离线可用
|
||||
* Runtime Module Resolver
|
||||
*/
|
||||
|
||||
import { TauriAPI } from '../api/tauri';
|
||||
|
||||
// Sanitize path by removing path traversal sequences and normalizing
|
||||
const sanitizePath = (path: string): string => {
|
||||
// Split by path separators, filter out '..' and empty segments, rejoin
|
||||
const segments = path.split(/[/\\]/).filter((segment) =>
|
||||
segment !== '..' && segment !== '.' && segment !== ''
|
||||
);
|
||||
// Use Windows backslash for consistency
|
||||
return segments.join('\\');
|
||||
};
|
||||
|
||||
// Check if we're in development mode
|
||||
const isDevelopment = (): boolean => {
|
||||
try {
|
||||
// Vite environment variable - this is the most reliable check
|
||||
const viteDev = (import.meta as any).env?.DEV === true;
|
||||
// Also check if MODE is 'development'
|
||||
const viteMode = (import.meta as any).env?.MODE === 'development';
|
||||
return viteDev || viteMode;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export interface RuntimeModule {
|
||||
type: 'javascript' | 'wasm' | 'binary';
|
||||
files: string[];
|
||||
sourcePath: string;
|
||||
}
|
||||
|
||||
export interface RuntimeConfig {
|
||||
runtime: {
|
||||
version: string;
|
||||
modules: Record<string, any>;
|
||||
};
|
||||
/**
|
||||
* 运行时模块清单
|
||||
* Module manifest for runtime modules
|
||||
*/
|
||||
export interface ModuleManifest {
|
||||
id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
dependencies: string[];
|
||||
hasRuntime: boolean;
|
||||
pluginExport?: string;
|
||||
requiresWasm?: boolean;
|
||||
wasmPaths?: string[];
|
||||
runtimeWasmPath?: string;
|
||||
externalDependencies?: string[];
|
||||
}
|
||||
|
||||
export class RuntimeResolver {
|
||||
private static instance: RuntimeResolver;
|
||||
private config: RuntimeConfig | null = null;
|
||||
private baseDir: string = '';
|
||||
private isDev: boolean = false; // Store dev mode state at initialization time
|
||||
private engineModulesPath: string = '';
|
||||
private initialized: boolean = false;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
@@ -62,67 +38,40 @@ export class RuntimeResolver {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the runtime resolver
|
||||
* 初始化运行时解析器
|
||||
* Initialize the runtime resolver
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
// Load runtime configuration
|
||||
const response = await fetch('/runtime.config.json');
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load runtime configuration: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (!contentType || !contentType.includes('application/json')) {
|
||||
throw new Error(`Invalid runtime configuration response type: ${contentType}. Expected JSON but received ${await response.text().then(t => t.substring(0, 100))}`);
|
||||
}
|
||||
this.config = await response.json();
|
||||
if (this.initialized) return;
|
||||
|
||||
// 查找 workspace 根目录
|
||||
// 查找工作区根目录 | Find workspace root
|
||||
const currentDir = await TauriAPI.getCurrentDir();
|
||||
const workspaceRoot = await this.findWorkspaceRoot(currentDir);
|
||||
this.baseDir = await this.findWorkspaceRoot(currentDir);
|
||||
|
||||
// 优先使用 workspace 中的开发文件(如果存在)
|
||||
// Prefer workspace dev files if they exist
|
||||
if (await this.hasRuntimeFilesInWorkspace(workspaceRoot)) {
|
||||
this.baseDir = workspaceRoot;
|
||||
this.isDev = true;
|
||||
} else {
|
||||
// 回退到打包的资源目录(生产模式)
|
||||
this.baseDir = await TauriAPI.getAppResourceDir();
|
||||
this.isDev = false;
|
||||
}
|
||||
// 查找引擎模块路径 | Find engine modules path
|
||||
this.engineModulesPath = await this.findEngineModulesPath();
|
||||
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if runtime files exist in workspace
|
||||
* 检查 workspace 中是否存在运行时文件
|
||||
*/
|
||||
private async hasRuntimeFilesInWorkspace(workspaceRoot: string): Promise<boolean> {
|
||||
const runtimePath = `${workspaceRoot}\\packages\\platform-web\\dist\\runtime.browser.js`;
|
||||
return await TauriAPI.pathExists(runtimePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find workspace root by looking for package.json or specific markers
|
||||
* 通过查找 package.json 或特定标记来找到工作区根目录
|
||||
* 查找工作区根目录
|
||||
* Find workspace root by looking for workspace markers
|
||||
*/
|
||||
private async findWorkspaceRoot(startPath: string): Promise<string> {
|
||||
let currentPath = startPath;
|
||||
|
||||
// Try to find the workspace root by looking for key files
|
||||
// We'll check up to 3 levels up from current directory
|
||||
for (let i = 0; i < 3; i++) {
|
||||
// Check if we're in src-tauri
|
||||
for (let i = 0; i < 5; i++) {
|
||||
// 检查是否在 src-tauri 目录 | Check if we're in src-tauri
|
||||
if (currentPath.endsWith('src-tauri')) {
|
||||
// Go up two levels to get to workspace root
|
||||
const parts = currentPath.split(/[/\\]/);
|
||||
parts.pop(); // Remove src-tauri
|
||||
parts.pop(); // Remove editor-app
|
||||
parts.pop(); // Remove packages
|
||||
parts.pop();
|
||||
parts.pop();
|
||||
parts.pop();
|
||||
return parts.join('\\');
|
||||
}
|
||||
|
||||
// Check for workspace markers
|
||||
// 检查工作区标记 | Check for workspace markers
|
||||
const workspaceMarkers = [
|
||||
`${currentPath}\\pnpm-workspace.yaml`,
|
||||
`${currentPath}\\packages\\editor-app`,
|
||||
@@ -141,103 +90,336 @@ export class RuntimeResolver {
|
||||
currentPath = parts.join('\\');
|
||||
}
|
||||
|
||||
// Fallback to current directory
|
||||
return startPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get runtime module files
|
||||
* 获取运行时模块文件
|
||||
* Find engine modules path (where compiled modules with module.json are)
|
||||
* 查找引擎模块路径(编译后的模块和 module.json 所在位置)
|
||||
*/
|
||||
async getModuleFiles(moduleName: string): Promise<RuntimeModule> {
|
||||
if (!this.config) {
|
||||
await this.initialize();
|
||||
private async findEngineModulesPath(): Promise<string> {
|
||||
// Try installed editor location first
|
||||
const installedPath = 'C:/Program Files/ESEngine Editor/engine';
|
||||
if (await TauriAPI.pathExists(`${installedPath}/index.json`)) {
|
||||
return installedPath;
|
||||
}
|
||||
|
||||
const moduleConfig = this.config!.runtime.modules[moduleName];
|
||||
if (!moduleConfig) {
|
||||
throw new Error(`Runtime module ${moduleName} not found in configuration`);
|
||||
// Try workspace packages directory (dev mode)
|
||||
const workspacePath = `${this.baseDir}\\packages`;
|
||||
if (await TauriAPI.pathExists(`${workspacePath}\\core\\module.json`)) {
|
||||
return workspacePath;
|
||||
}
|
||||
|
||||
const files: string[] = [];
|
||||
let sourcePath: string;
|
||||
|
||||
if (this.isDev) {
|
||||
// Development mode - use relative paths from workspace root
|
||||
const devPath = moduleConfig.development.path;
|
||||
const sanitizedPath = sanitizePath(devPath);
|
||||
sourcePath = `${this.baseDir}\\packages\\${sanitizedPath}`;
|
||||
|
||||
if (moduleConfig.main) {
|
||||
files.push(`${sourcePath}\\${moduleConfig.main}`);
|
||||
}
|
||||
if (moduleConfig.files) {
|
||||
for (const file of moduleConfig.files) {
|
||||
files.push(`${sourcePath}\\${file}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Production mode - files are bundled with the app
|
||||
sourcePath = this.baseDir;
|
||||
|
||||
if (moduleConfig.main) {
|
||||
files.push(`${sourcePath}\\${moduleConfig.main}`);
|
||||
}
|
||||
if (moduleConfig.files) {
|
||||
for (const file of moduleConfig.files) {
|
||||
files.push(`${sourcePath}\\${file}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: moduleConfig.type,
|
||||
files,
|
||||
sourcePath
|
||||
};
|
||||
return workspacePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare runtime files for browser preview
|
||||
* 为浏览器预览准备运行时文件
|
||||
* Get list of available runtime modules
|
||||
* 获取可用的运行时模块列表
|
||||
*
|
||||
* 开发模式:从本地 workspace 复制
|
||||
* 生产模式:从编辑器内置资源复制
|
||||
* Scans the packages directory for module.json files instead of hardcoding
|
||||
* 扫描 packages 目录查找 module.json 文件,而不是硬编码
|
||||
*/
|
||||
async prepareRuntimeFiles(targetDir: string): Promise<void> {
|
||||
async getAvailableModules(): Promise<ModuleManifest[]> {
|
||||
if (!this.initialized) {
|
||||
await this.initialize();
|
||||
}
|
||||
|
||||
const modules: ModuleManifest[] = [];
|
||||
|
||||
// Try to read index.json if it exists (installed editor)
|
||||
const indexPath = `${this.engineModulesPath}\\index.json`;
|
||||
if (await TauriAPI.pathExists(indexPath)) {
|
||||
try {
|
||||
const indexContent = await TauriAPI.readFileContent(indexPath);
|
||||
const indexData = JSON.parse(indexContent) as { modules: ModuleManifest[] };
|
||||
return indexData.modules.filter(m => m.hasRuntime);
|
||||
} catch (e) {
|
||||
console.warn('[RuntimeResolver] Failed to read index.json:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Scan packages directory for module.json files
|
||||
const packageEntries = await TauriAPI.listDirectory(this.engineModulesPath);
|
||||
for (const entry of packageEntries) {
|
||||
if (!entry.is_dir) continue;
|
||||
|
||||
const manifestPath = `${this.engineModulesPath}\\${entry.name}\\module.json`;
|
||||
if (await TauriAPI.pathExists(manifestPath)) {
|
||||
try {
|
||||
const content = await TauriAPI.readFileContent(manifestPath);
|
||||
const manifest = JSON.parse(content) as ModuleManifest;
|
||||
if (manifest.hasRuntime !== false) {
|
||||
modules.push(manifest);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`[RuntimeResolver] Failed to read module.json for ${entry.name}:`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by dependencies
|
||||
return this.sortModulesByDependencies(modules);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort modules by dependencies (topological sort)
|
||||
* 按依赖排序模块(拓扑排序)
|
||||
*/
|
||||
private sortModulesByDependencies(modules: ModuleManifest[]): ModuleManifest[] {
|
||||
const sorted: ModuleManifest[] = [];
|
||||
const visited = new Set<string>();
|
||||
const moduleMap = new Map(modules.map(m => [m.id, m]));
|
||||
|
||||
const visit = (module: ModuleManifest) => {
|
||||
if (visited.has(module.id)) return;
|
||||
visited.add(module.id);
|
||||
for (const depId of (module.dependencies || [])) {
|
||||
const dep = moduleMap.get(depId);
|
||||
if (dep) visit(dep);
|
||||
}
|
||||
sorted.push(module);
|
||||
};
|
||||
|
||||
for (const module of modules) {
|
||||
visit(module);
|
||||
}
|
||||
return sorted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare runtime files for browser preview using ES Modules
|
||||
* 使用 ES 模块为浏览器预览准备运行时文件
|
||||
*
|
||||
* Creates libs/{moduleId}/{moduleId}.js structure matching published builds
|
||||
* 创建与发布构建一致的 libs/{moduleId}/{moduleId}.js 结构
|
||||
*/
|
||||
async prepareRuntimeFiles(targetDir: string): Promise<{ modules: ModuleManifest[], importMap: Record<string, string> }> {
|
||||
if (!this.initialized) {
|
||||
await this.initialize();
|
||||
}
|
||||
|
||||
// Ensure target directory exists
|
||||
const dirExists = await TauriAPI.pathExists(targetDir);
|
||||
if (!dirExists) {
|
||||
if (!await TauriAPI.pathExists(targetDir)) {
|
||||
await TauriAPI.createDirectory(targetDir);
|
||||
}
|
||||
|
||||
// Copy platform-web runtime
|
||||
const platformWeb = await this.getModuleFiles('platform-web');
|
||||
for (const srcFile of platformWeb.files) {
|
||||
const filename = srcFile.split(/[/\\]/).pop() || '';
|
||||
const dstFile = `${targetDir}\\${filename}`;
|
||||
const libsDir = `${targetDir}\\libs`;
|
||||
if (!await TauriAPI.pathExists(libsDir)) {
|
||||
await TauriAPI.createDirectory(libsDir);
|
||||
}
|
||||
|
||||
const srcExists = await TauriAPI.pathExists(srcFile);
|
||||
if (srcExists) {
|
||||
const modules = await this.getAvailableModules();
|
||||
const importMap: Record<string, string> = {};
|
||||
const copiedModules: string[] = [];
|
||||
|
||||
// Copy each module's dist files
|
||||
for (const module of modules) {
|
||||
const moduleDistDir = `${this.engineModulesPath}\\${module.id}\\dist`;
|
||||
const moduleSrcFile = `${moduleDistDir}\\index.mjs`;
|
||||
|
||||
// Check for index.mjs or index.js
|
||||
let srcFile = moduleSrcFile;
|
||||
if (!await TauriAPI.pathExists(srcFile)) {
|
||||
srcFile = `${moduleDistDir}\\index.js`;
|
||||
}
|
||||
|
||||
if (await TauriAPI.pathExists(srcFile)) {
|
||||
const dstModuleDir = `${libsDir}\\${module.id}`;
|
||||
if (!await TauriAPI.pathExists(dstModuleDir)) {
|
||||
await TauriAPI.createDirectory(dstModuleDir);
|
||||
}
|
||||
|
||||
const dstFile = `${dstModuleDir}\\${module.id}.js`;
|
||||
await TauriAPI.copyFile(srcFile, dstFile);
|
||||
} else {
|
||||
throw new Error(`Runtime file not found: ${srcFile}`);
|
||||
|
||||
// Copy all chunk files (code splitting creates chunk-*.js files)
|
||||
// 复制所有 chunk 文件(代码分割会创建 chunk-*.js 文件)
|
||||
await this.copyChunkFiles(moduleDistDir, dstModuleDir);
|
||||
|
||||
// Add to import map
|
||||
importMap[`@esengine/${module.id}`] = `./libs/${module.id}/${module.id}.js`;
|
||||
|
||||
// Also add common aliases
|
||||
if (module.id === 'core') {
|
||||
importMap['@esengine/ecs-framework'] = `./libs/${module.id}/${module.id}.js`;
|
||||
}
|
||||
if (module.id === 'math') {
|
||||
importMap['@esengine/ecs-framework-math'] = `./libs/${module.id}/${module.id}.js`;
|
||||
}
|
||||
|
||||
copiedModules.push(module.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Copy engine WASM files
|
||||
const engine = await this.getModuleFiles('engine');
|
||||
for (const srcFile of engine.files) {
|
||||
const filename = srcFile.split(/[/\\]/).pop() || '';
|
||||
const dstFile = `${targetDir}\\${filename}`;
|
||||
// Copy external dependencies (e.g., rapier2d)
|
||||
await this.copyExternalDependencies(modules, libsDir, importMap);
|
||||
|
||||
const srcExists = await TauriAPI.pathExists(srcFile);
|
||||
if (srcExists) {
|
||||
await TauriAPI.copyFile(srcFile, dstFile);
|
||||
} else {
|
||||
throw new Error(`Engine file not found: ${srcFile}`);
|
||||
// Copy engine WASM files to libs/es-engine/
|
||||
await this.copyEngineWasm(libsDir);
|
||||
|
||||
// Copy module-specific WASM files
|
||||
await this.copyModuleWasm(modules, targetDir);
|
||||
|
||||
console.log(`[RuntimeResolver] Prepared ${copiedModules.length} modules for browser preview`);
|
||||
|
||||
return { modules, importMap };
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy chunk files from dist directory (for code-split modules)
|
||||
* 复制 dist 目录中的 chunk 文件(用于代码分割的模块)
|
||||
*/
|
||||
private async copyChunkFiles(srcDir: string, dstDir: string): Promise<void> {
|
||||
try {
|
||||
const entries = await TauriAPI.listDirectory(srcDir);
|
||||
for (const entry of entries) {
|
||||
// Copy chunk-*.js files and any other .js files (except index.*)
|
||||
if (!entry.is_dir && entry.name.endsWith('.js') && !entry.name.startsWith('index.')) {
|
||||
const srcFile = `${srcDir}\\${entry.name}`;
|
||||
const dstFile = `${dstDir}\\${entry.name}`;
|
||||
await TauriAPI.copyFile(srcFile, dstFile);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore errors - some modules may not have chunk files
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy external dependencies like rapier2d
|
||||
* 复制外部依赖如 rapier2d
|
||||
*/
|
||||
private async copyExternalDependencies(
|
||||
modules: ModuleManifest[],
|
||||
libsDir: string,
|
||||
importMap: Record<string, string>
|
||||
): Promise<void> {
|
||||
const externalDeps = new Set<string>();
|
||||
for (const m of modules) {
|
||||
if (m.externalDependencies) {
|
||||
for (const dep of m.externalDependencies) {
|
||||
externalDeps.add(dep);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const dep of externalDeps) {
|
||||
const depId = dep.startsWith('@esengine/') ? dep.slice(10) : dep.replace(/^@[^/]+\//, '');
|
||||
const srcDistDir = `${this.engineModulesPath}\\${depId}\\dist`;
|
||||
let srcFile = `${srcDistDir}\\index.mjs`;
|
||||
if (!await TauriAPI.pathExists(srcFile)) {
|
||||
srcFile = `${srcDistDir}\\index.js`;
|
||||
}
|
||||
|
||||
if (await TauriAPI.pathExists(srcFile)) {
|
||||
const dstModuleDir = `${libsDir}\\${depId}`;
|
||||
if (!await TauriAPI.pathExists(dstModuleDir)) {
|
||||
await TauriAPI.createDirectory(dstModuleDir);
|
||||
}
|
||||
|
||||
const dstFile = `${dstModuleDir}\\${depId}.js`;
|
||||
await TauriAPI.copyFile(srcFile, dstFile);
|
||||
|
||||
// Copy chunk files for external dependencies too
|
||||
await this.copyChunkFiles(srcDistDir, dstModuleDir);
|
||||
|
||||
importMap[dep] = `./libs/${depId}/${depId}.js`;
|
||||
console.log(`[RuntimeResolver] Copied external dependency: ${depId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy engine WASM files
|
||||
* 复制引擎 WASM 文件
|
||||
*/
|
||||
private async copyEngineWasm(libsDir: string): Promise<void> {
|
||||
const esEngineDir = `${libsDir}\\es-engine`;
|
||||
if (!await TauriAPI.pathExists(esEngineDir)) {
|
||||
await TauriAPI.createDirectory(esEngineDir);
|
||||
}
|
||||
|
||||
// Try different locations for engine WASM
|
||||
const wasmSearchPaths = [
|
||||
`${this.baseDir}\\packages\\engine\\pkg`,
|
||||
`${this.engineModulesPath}\\..\\..\\engine\\pkg`,
|
||||
'C:/Program Files/ESEngine Editor/wasm'
|
||||
];
|
||||
|
||||
const filesToCopy = ['es_engine_bg.wasm', 'es_engine.js', 'es_engine_bg.js'];
|
||||
|
||||
for (const searchPath of wasmSearchPaths) {
|
||||
if (await TauriAPI.pathExists(searchPath)) {
|
||||
for (const file of filesToCopy) {
|
||||
const srcFile = `${searchPath}\\${file}`;
|
||||
if (await TauriAPI.pathExists(srcFile)) {
|
||||
const dstFile = `${esEngineDir}\\${file}`;
|
||||
await TauriAPI.copyFile(srcFile, dstFile);
|
||||
}
|
||||
}
|
||||
console.log('[RuntimeResolver] Copied engine WASM from:', searchPath);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.warn('[RuntimeResolver] Engine WASM files not found');
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy module-specific WASM files (e.g., physics)
|
||||
* 复制模块特定的 WASM 文件(如物理)
|
||||
*/
|
||||
private async copyModuleWasm(modules: ModuleManifest[], targetDir: string): Promise<void> {
|
||||
for (const module of modules) {
|
||||
if (!module.requiresWasm || !module.wasmPaths?.length) continue;
|
||||
|
||||
const runtimePath = module.runtimeWasmPath || `wasm/${module.wasmPaths[0]}`;
|
||||
const dstPath = `${targetDir}\\${runtimePath.replace(/\//g, '\\')}`;
|
||||
const dstDir = dstPath.substring(0, dstPath.lastIndexOf('\\'));
|
||||
|
||||
if (!await TauriAPI.pathExists(dstDir)) {
|
||||
await TauriAPI.createDirectory(dstDir);
|
||||
}
|
||||
|
||||
// Search for the WASM file
|
||||
const wasmPath = module.wasmPaths[0];
|
||||
if (!wasmPath) continue;
|
||||
const wasmFileName = wasmPath.split(/[/\\]/).pop() || wasmPath;
|
||||
|
||||
// Build search paths - check module's own pkg, external deps, and common locations
|
||||
const searchPaths: string[] = [
|
||||
`${this.engineModulesPath}\\${module.id}\\pkg\\${wasmFileName}`,
|
||||
`${this.baseDir}\\packages\\${module.id}\\pkg\\${wasmFileName}`,
|
||||
];
|
||||
|
||||
// Check external dependencies for WASM (e.g., physics-rapier2d uses rapier2d's WASM)
|
||||
if (module.externalDependencies) {
|
||||
for (const dep of module.externalDependencies) {
|
||||
const depId = dep.startsWith('@esengine/') ? dep.slice(10) : dep.replace(/^@[^/]+\//, '');
|
||||
searchPaths.push(`${this.engineModulesPath}\\${depId}\\pkg\\${wasmFileName}`);
|
||||
searchPaths.push(`${this.baseDir}\\packages\\${depId}\\pkg\\${wasmFileName}`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const srcPath of searchPaths) {
|
||||
if (await TauriAPI.pathExists(srcPath)) {
|
||||
await TauriAPI.copyFile(srcPath, dstPath);
|
||||
console.log(`[RuntimeResolver] Copied ${module.id} WASM to ${runtimePath}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate import map for runtime HTML
|
||||
* 生成运行时 HTML 的 import map
|
||||
*/
|
||||
generateImportMapHtml(importMap: Record<string, string>): string {
|
||||
return `<script type="importmap">
|
||||
${JSON.stringify({ imports: importMap }, null, 2).split('\n').join('\n ')}
|
||||
</script>`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -247,4 +429,12 @@ export class RuntimeResolver {
|
||||
getBaseDir(): string {
|
||||
return this.baseDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get engine modules path
|
||||
* 获取引擎模块路径
|
||||
*/
|
||||
getEngineModulesPath(): string {
|
||||
return this.engineModulesPath;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user