2025-11-23 14:49:37 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* Runtime Module Resolver
|
|
|
|
|
|
* 运行时模块解析器
|
|
|
|
|
|
*
|
|
|
|
|
|
* Resolves runtime module paths based on environment and configuration
|
|
|
|
|
|
* 根据环境和配置解析运行时模块路径
|
2025-11-27 20:42:46 +08:00
|
|
|
|
*
|
|
|
|
|
|
* 运行时文件打包在编辑器内,离线可用
|
2025-11-23 14:49:37 +08:00
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
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 !== ''
|
|
|
|
|
|
);
|
2025-11-27 20:42:46 +08:00
|
|
|
|
// Use Windows backslash for consistency
|
|
|
|
|
|
return segments.join('\\');
|
2025-11-23 14:49:37 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Check if we're in development mode
|
|
|
|
|
|
const isDevelopment = (): boolean => {
|
|
|
|
|
|
try {
|
2025-11-27 20:42:46 +08:00
|
|
|
|
// 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;
|
2025-11-23 14:49:37 +08:00
|
|
|
|
} catch {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export interface RuntimeModule {
|
|
|
|
|
|
type: 'javascript' | 'wasm' | 'binary';
|
|
|
|
|
|
files: string[];
|
|
|
|
|
|
sourcePath: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface RuntimeConfig {
|
|
|
|
|
|
runtime: {
|
|
|
|
|
|
version: string;
|
|
|
|
|
|
modules: Record<string, any>;
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export class RuntimeResolver {
|
|
|
|
|
|
private static instance: RuntimeResolver;
|
|
|
|
|
|
private config: RuntimeConfig | null = null;
|
|
|
|
|
|
private baseDir: string = '';
|
2025-11-27 20:42:46 +08:00
|
|
|
|
private isDev: boolean = false; // Store dev mode state at initialization time
|
2025-11-23 14:49:37 +08:00
|
|
|
|
|
|
|
|
|
|
private constructor() {}
|
|
|
|
|
|
|
|
|
|
|
|
static getInstance(): RuntimeResolver {
|
|
|
|
|
|
if (!RuntimeResolver.instance) {
|
|
|
|
|
|
RuntimeResolver.instance = new RuntimeResolver();
|
|
|
|
|
|
}
|
|
|
|
|
|
return RuntimeResolver.instance;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Initialize the runtime resolver
|
|
|
|
|
|
* 初始化运行时解析器
|
|
|
|
|
|
*/
|
|
|
|
|
|
async initialize(): Promise<void> {
|
|
|
|
|
|
// Load runtime configuration
|
|
|
|
|
|
const response = await fetch('/runtime.config.json');
|
2025-11-23 21:45:10 +08:00
|
|
|
|
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))}`);
|
|
|
|
|
|
}
|
2025-11-23 14:49:37 +08:00
|
|
|
|
this.config = await response.json();
|
|
|
|
|
|
|
2025-11-27 20:42:46 +08:00
|
|
|
|
// 查找 workspace 根目录
|
|
|
|
|
|
const currentDir = await TauriAPI.getCurrentDir();
|
|
|
|
|
|
const workspaceRoot = await this.findWorkspaceRoot(currentDir);
|
|
|
|
|
|
|
|
|
|
|
|
// 优先使用 workspace 中的开发文件(如果存在)
|
|
|
|
|
|
// Prefer workspace dev files if they exist
|
|
|
|
|
|
if (await this.hasRuntimeFilesInWorkspace(workspaceRoot)) {
|
|
|
|
|
|
this.baseDir = workspaceRoot;
|
|
|
|
|
|
this.isDev = true;
|
2025-11-23 14:49:37 +08:00
|
|
|
|
} else {
|
2025-11-27 20:42:46 +08:00
|
|
|
|
// 回退到打包的资源目录(生产模式)
|
2025-11-23 14:49:37 +08:00
|
|
|
|
this.baseDir = await TauriAPI.getAppResourceDir();
|
2025-11-27 20:42:46 +08:00
|
|
|
|
this.isDev = false;
|
2025-11-23 14:49:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-27 20:42:46 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 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`;
|
2025-11-28 10:32:28 +08:00
|
|
|
|
return await TauriAPI.pathExists(runtimePath);
|
2025-11-27 20:42:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-23 14:49:37 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* Find workspace root by looking for package.json or specific markers
|
|
|
|
|
|
* 通过查找 package.json 或特定标记来找到工作区根目录
|
|
|
|
|
|
*/
|
|
|
|
|
|
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
|
|
|
|
|
|
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
|
|
|
|
|
|
return parts.join('\\');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Check for workspace markers
|
|
|
|
|
|
const workspaceMarkers = [
|
|
|
|
|
|
`${currentPath}\\pnpm-workspace.yaml`,
|
|
|
|
|
|
`${currentPath}\\packages\\editor-app`,
|
|
|
|
|
|
`${currentPath}\\packages\\platform-web`
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
for (const marker of workspaceMarkers) {
|
|
|
|
|
|
if (await TauriAPI.pathExists(marker)) {
|
|
|
|
|
|
return currentPath;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Go up one level
|
|
|
|
|
|
const parts = currentPath.split(/[/\\]/);
|
|
|
|
|
|
parts.pop();
|
|
|
|
|
|
currentPath = parts.join('\\');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Fallback to current directory
|
|
|
|
|
|
return startPath;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Get runtime module files
|
|
|
|
|
|
* 获取运行时模块文件
|
|
|
|
|
|
*/
|
|
|
|
|
|
async getModuleFiles(moduleName: string): Promise<RuntimeModule> {
|
|
|
|
|
|
if (!this.config) {
|
|
|
|
|
|
await this.initialize();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const moduleConfig = this.config!.runtime.modules[moduleName];
|
|
|
|
|
|
if (!moduleConfig) {
|
|
|
|
|
|
throw new Error(`Runtime module ${moduleName} not found in configuration`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const files: string[] = [];
|
|
|
|
|
|
let sourcePath: string;
|
|
|
|
|
|
|
2025-11-27 20:42:46 +08:00
|
|
|
|
if (this.isDev) {
|
2025-11-23 14:49:37 +08:00
|
|
|
|
// Development mode - use relative paths from workspace root
|
|
|
|
|
|
const devPath = moduleConfig.development.path;
|
2025-11-27 20:42:46 +08:00
|
|
|
|
const sanitizedPath = sanitizePath(devPath);
|
|
|
|
|
|
sourcePath = `${this.baseDir}\\packages\\${sanitizedPath}`;
|
2025-11-23 14:49:37 +08:00
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Prepare runtime files for browser preview
|
|
|
|
|
|
* 为浏览器预览准备运行时文件
|
2025-11-27 20:42:46 +08:00
|
|
|
|
*
|
|
|
|
|
|
* 开发模式:从本地 workspace 复制
|
|
|
|
|
|
* 生产模式:从编辑器内置资源复制
|
2025-11-23 14:49:37 +08:00
|
|
|
|
*/
|
|
|
|
|
|
async prepareRuntimeFiles(targetDir: string): Promise<void> {
|
|
|
|
|
|
// Ensure target directory exists
|
|
|
|
|
|
const dirExists = await TauriAPI.pathExists(targetDir);
|
|
|
|
|
|
if (!dirExists) {
|
|
|
|
|
|
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}`;
|
|
|
|
|
|
|
2025-11-27 20:42:46 +08:00
|
|
|
|
const srcExists = await TauriAPI.pathExists(srcFile);
|
|
|
|
|
|
if (srcExists) {
|
2025-11-23 14:49:37 +08:00
|
|
|
|
await TauriAPI.copyFile(srcFile, dstFile);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
throw new Error(`Runtime file not found: ${srcFile}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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}`;
|
|
|
|
|
|
|
2025-11-27 20:42:46 +08:00
|
|
|
|
const srcExists = await TauriAPI.pathExists(srcFile);
|
|
|
|
|
|
if (srcExists) {
|
2025-11-23 14:49:37 +08:00
|
|
|
|
await TauriAPI.copyFile(srcFile, dstFile);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
throw new Error(`Engine file not found: ${srcFile}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Get workspace root directory
|
|
|
|
|
|
* 获取工作区根目录
|
|
|
|
|
|
*/
|
|
|
|
|
|
getBaseDir(): string {
|
|
|
|
|
|
return this.baseDir;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|