Files
esengine/packages/editor-app/src/services/PluginLoader.ts

219 lines
9.1 KiB
TypeScript
Raw Normal View History

import { EditorPluginManager, LocaleService, MessageHub } from '@esengine/editor-core';
import type { IEditorPlugin } from '@esengine/editor-core';
import { Core } from '@esengine/ecs-framework';
import { TauriAPI } from '../api/tauri';
interface PluginPackageJson {
name: string;
version: string;
main?: string;
module?: string;
exports?: {
'.': {
import?: string;
require?: string;
development?: {
types?: string;
import?: string;
};
}
};
}
export class PluginLoader {
private loadedPluginNames: Set<string> = new Set();
async loadProjectPlugins(projectPath: string, pluginManager: EditorPluginManager): Promise<void> {
const pluginsPath = `${projectPath}/plugins`;
try {
const exists = await TauriAPI.pathExists(pluginsPath);
if (!exists) {
console.log('[PluginLoader] No plugins directory found');
return;
}
const entries = await TauriAPI.listDirectory(pluginsPath);
const pluginDirs = entries.filter((entry) => entry.is_dir && !entry.name.startsWith('.'));
console.log('[PluginLoader] Found plugin directories:', pluginDirs.map((d) => d.name));
for (const entry of pluginDirs) {
const pluginPath = `${pluginsPath}/${entry.name}`;
await this.loadPlugin(pluginPath, entry.name, pluginManager);
}
} catch (error) {
console.error('[PluginLoader] Failed to load project plugins:', error);
}
}
private async loadPlugin(pluginPath: string, pluginDirName: string, pluginManager: EditorPluginManager): Promise<void> {
try {
const packageJsonPath = `${pluginPath}/package.json`;
const packageJsonExists = await TauriAPI.pathExists(packageJsonPath);
if (!packageJsonExists) {
console.warn(`[PluginLoader] No package.json found in ${pluginPath}`);
return;
}
const packageJsonContent = await TauriAPI.readFileContent(packageJsonPath);
const packageJson: PluginPackageJson = JSON.parse(packageJsonContent);
if (this.loadedPluginNames.has(packageJson.name)) {
try {
await pluginManager.uninstallEditor(packageJson.name);
this.loadedPluginNames.delete(packageJson.name);
} catch (error) {
console.error(`[PluginLoader] Failed to uninstall existing plugin ${packageJson.name}:`, error);
}
}
let entryPoint = 'src/index.ts';
if (packageJson.exports?.['.']?.development?.import) {
entryPoint = packageJson.exports['.'].development.import;
} else if (packageJson.exports?.['.']?.import) {
const importPath = packageJson.exports['.'].import;
if (importPath.startsWith('src/')) {
entryPoint = importPath;
} else {
const srcPath = importPath.replace('dist/', 'src/').replace('.js', '.ts');
const srcExists = await TauriAPI.pathExists(`${pluginPath}/${srcPath}`);
entryPoint = srcExists ? srcPath : importPath;
}
} else if (packageJson.module) {
const srcPath = packageJson.module.replace('dist/', 'src/').replace('.js', '.ts');
const srcExists = await TauriAPI.pathExists(`${pluginPath}/${srcPath}`);
entryPoint = srcExists ? srcPath : packageJson.module;
} else if (packageJson.main) {
const srcPath = packageJson.main.replace('dist/', 'src/').replace('.js', '.ts');
const srcExists = await TauriAPI.pathExists(`${pluginPath}/${srcPath}`);
entryPoint = srcExists ? srcPath : packageJson.main;
}
// 移除开头的 ./
entryPoint = entryPoint.replace(/^\.\//, '');
// 添加时间戳参数强制重新加载模块(避免缓存)
const timestamp = Date.now();
const moduleUrl = `/@user-project/plugins/${pluginDirName}/${entryPoint}?t=${timestamp}`;
console.log(`[PluginLoader] Loading plugin from: ${moduleUrl}`);
const module = await import(/* @vite-ignore */ moduleUrl);
console.log('[PluginLoader] Module loaded successfully');
let pluginInstance: IEditorPlugin | null = null;
try {
pluginInstance = this.findPluginInstance(module);
} catch (findError) {
console.error('[PluginLoader] Error finding plugin instance:', findError);
console.error('[PluginLoader] Module object:', module);
return;
}
if (!pluginInstance) {
console.error(`[PluginLoader] No plugin instance found in ${packageJson.name}`);
return;
}
await pluginManager.installEditor(pluginInstance);
this.loadedPluginNames.add(packageJson.name);
// 同步插件的语言设置
try {
const localeService = Core.services.resolve(LocaleService);
const currentLocale = localeService.getCurrentLocale();
if (pluginInstance.setLocale) {
pluginInstance.setLocale(currentLocale);
console.log(`[PluginLoader] Set locale for plugin ${packageJson.name}: ${currentLocale}`);
}
} catch (error) {
console.warn(`[PluginLoader] Failed to set locale for plugin ${packageJson.name}:`, error);
}
// 通知节点面板重新加载模板
try {
const messageHub = Core.services.resolve(MessageHub);
const localeService = Core.services.resolve(LocaleService);
messageHub.publish('locale:changed', { locale: localeService.getCurrentLocale() });
console.log(`[PluginLoader] Published locale:changed event for plugin ${packageJson.name}`);
} catch (error) {
console.warn('[PluginLoader] Failed to publish locale:changed event:', error);
}
console.log(`[PluginLoader] Successfully loaded plugin: ${packageJson.name}`);
} catch (error) {
console.error(`[PluginLoader] Failed to load plugin from ${pluginPath}:`, error);
if (error instanceof Error) {
console.error('[PluginLoader] Error stack:', error.stack);
}
}
}
private findPluginInstance(module: any): IEditorPlugin | null {
console.log('[PluginLoader] Module exports:', Object.keys(module));
if (module.default && this.isPluginInstance(module.default)) {
console.log('[PluginLoader] Found plugin in default export');
return module.default;
}
for (const key of Object.keys(module)) {
const value = module[key];
if (value && this.isPluginInstance(value)) {
console.log(`[PluginLoader] Found plugin in export: ${key}`);
return value;
}
}
console.error('[PluginLoader] No valid plugin instance found. Exports:', module);
return null;
}
private isPluginInstance(obj: any): obj is IEditorPlugin {
try {
if (!obj || typeof obj !== 'object') {
return false;
}
const hasRequiredProperties =
typeof obj.name === 'string' &&
typeof obj.version === 'string' &&
typeof obj.displayName === 'string' &&
typeof obj.category === 'string' &&
typeof obj.install === 'function' &&
typeof obj.uninstall === 'function';
if (!hasRequiredProperties) {
console.log('[PluginLoader] Object is not a valid plugin:', {
hasName: typeof obj.name === 'string',
hasVersion: typeof obj.version === 'string',
hasDisplayName: typeof obj.displayName === 'string',
hasCategory: typeof obj.category === 'string',
hasInstall: typeof obj.install === 'function',
hasUninstall: typeof obj.uninstall === 'function',
objectType: typeof obj,
objectConstructor: obj?.constructor?.name
});
}
return hasRequiredProperties;
} catch (error) {
console.error('[PluginLoader] Error in isPluginInstance:', error);
return false;
}
}
async unloadProjectPlugins(pluginManager: EditorPluginManager): Promise<void> {
for (const pluginName of this.loadedPluginNames) {
try {
await pluginManager.uninstallEditor(pluginName);
} catch (error) {
console.error(`[PluginLoader] Failed to unload plugin ${pluginName}:`, error);
}
}
this.loadedPluginNames.clear();
}
}