import React, { useEffect } from 'react'; import { MatrixClient } from 'matrix-js-sdk'; import { Plugin, createPluginContext } from '@paarrot/plugin-manager'; import { pluginMarketplaceManager, pluginRegistry } from './PluginAPI'; import { sendNotification } from '../../../utils/tauri'; interface PluginLoaderProps { matrixClient: MatrixClient; children: React.ReactNode; } /** * Component that loads and initializes enabled plugins. * Should be placed high in the component tree after MatrixClient is available. */ export function PluginLoader({ matrixClient, children }: PluginLoaderProps) { useEffect(() => { const loadPlugins = async () => { try { console.log('[PluginLoader] Loading plugins...'); if (!window.electron?.plugins) { console.log('[PluginLoader] Running in web mode, plugins not supported'); return; } const pluginsToLoad = await pluginMarketplaceManager.listEnabledPlugins(); console.log('[PluginLoader] Plugins to load:', pluginsToLoad.length); for (const installedPlugin of pluginsToLoad) { try { console.log(`[PluginLoader] Loading plugin: ${installedPlugin.id}`); const pluginModule = await loadPluginModule(installedPlugin.id); const plugin = normalisePlugin(pluginModule); if (!plugin || typeof plugin !== 'object') { console.error( `[PluginLoader] Plugin ${installedPlugin.id} does not export a valid plugin object` ); continue; } if (typeof plugin.onLoad !== 'function') { console.error( `[PluginLoader] Plugin ${installedPlugin.id} does not have an onLoad function` ); continue; } const context = createPluginContext( { pluginId: installedPlugin.id, eventClient: matrixClient as any, onNotify: async (opts) => { await sendNotification({ title: opts.title, body: opts.body, path: undefined, }); }, }, pluginRegistry ); const compatContext = Object.assign(context as Record, { matrix: { on: (eventType: string, handler: (...args: unknown[]) => void) => matrixClient.on(eventType as any, handler as any), off: (eventType: string, handler: (...args: unknown[]) => void) => matrixClient.off(eventType as any, handler as any), }, React, }); pluginRegistry.registerPlugin(installedPlugin.id, plugin, context); await plugin.onLoad(compatContext as any); console.log(`[PluginLoader] Successfully loaded plugin: ${installedPlugin.id}`); } catch (error) { console.error(`[PluginLoader] Failed to load plugin ${installedPlugin.id}:`, error); pluginRegistry.addLog(installedPlugin.id, 'error', ['Failed to load:', error]); } } } catch (error) { console.error('[PluginLoader] Error loading plugins:', error); } }; loadPlugins(); return () => { console.log('[PluginLoader] Cleaning up plugins'); pluginRegistry.clear(); }; }, [matrixClient]); return <>{children}; } /** * Reads and evaluates a plugin's JS module from the Electron filesystem. * Supports both CommonJS and ESM-style exports. */ async function loadPluginModule(pluginId: string): Promise { if (!window.electron?.plugins) { throw new Error('Plugins are only supported in the desktop app'); } const response = await window.electron.plugins.readPluginCode(pluginId); if (!response.success || !response.data) { throw new Error(response.error ?? 'Failed to read plugin code'); } const pluginExports: any = {}; const mod = { exports: pluginExports }; try { // eslint-disable-next-line no-new-func const pluginFunction = new Function('module', 'exports', response.data); pluginFunction(mod, pluginExports); } catch (error) { if (!(error instanceof SyntaxError)) { throw error; } const dataUrl = `data:text/javascript;charset=utf-8,${encodeURIComponent(response.data)}`; const importedModule = await import(/* @vite-ignore */ dataUrl); return importedModule; } return mod.exports; } /** * Normalizes raw plugin exports to current plugin interface. * Supports native `onLoad` and legacy `activate` / `deactivate` plugins. */ function normalisePlugin(raw: Record): Plugin { if (typeof raw.onLoad === 'function') { return raw as unknown as Plugin; } if (typeof raw.activate === 'function') { const activate = raw.activate as (ctx: unknown) => void | Promise; const deactivate = raw.deactivate as ((ctx?: unknown) => void | Promise) | undefined; return { name: typeof raw.name === 'string' ? raw.name : undefined, version: typeof raw.version === 'string' ? raw.version : undefined, onLoad: async (ctx) => { const legacyCtx = Object.assign(Object.create(ctx as object), { registerHook: (name: string) => ctx.log(`[compat] registerHook("${name}") - not supported`), runHook: (name: string) => { ctx.log(`[compat] runHook("${name}")`); return Promise.resolve([]); }, getConfig: (key: string) => ctx.settings.get(key), on: (event: string, handler: (...args: unknown[]) => void) => ctx.events.on(event, handler), }); await activate(legacyCtx); }, onUnload: deactivate ? async () => { await deactivate(); } : undefined, }; } return raw as unknown as Plugin; }