Something went wrong. Try again.
The agent that grows with you
Something went wrong. Try again.
4.0 kB · 123 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124/** * usePlugins hook — discovers and loads dashboard plugins. * * 1. Fetches plugin manifests from GET /api/dashboard/plugins * 2. Injects CSS <link> tags for plugins that declare css * 3. Loads plugin JS bundles via <script> tags * 4. Waits for plugins to call register() and resolves them */
import { useState, useEffect, useRef } from "react";import { api } from "@/lib/api";import type { PluginManifest, RegisteredPlugin } from "./types";import { getPluginComponent, onPluginRegistered, notifyPluginRegistry, setPluginLoadError,} from "./registry";
export function usePlugins() { const [manifests, setManifests] = useState<PluginManifest[]>([]); const [plugins, setPlugins] = useState<RegisteredPlugin[]>([]); const [loading, setLoading] = useState(true); const loadedScripts = useRef<Set<string>>(new Set());
// Fetch manifests on mount. useEffect(() => { api .getPlugins() .then((list) => { setManifests(list); if (list.length === 0) setLoading(false); }) .catch(() => setLoading(false)); }, []);
// Load plugin assets when manifests arrive. useEffect(() => { if (manifests.length === 0) return;
const injectedScripts: HTMLScriptElement[] = [];
for (const manifest of manifests) { // Inject CSS if specified. if (manifest.css) { const cssUrl = `/dashboard-plugins/${manifest.name}/${manifest.css}`; if (!document.querySelector(`link[href="${cssUrl}"]`)) { const link = document.createElement("link"); link.rel = "stylesheet"; link.href = cssUrl; document.head.appendChild(link); } }
// Load JS bundle. In dev, cache-bust so Vite HMR can clear the // in-memory registry while the browser would otherwise never // re-execute a previously cached <script> URL. const baseUrl = `/dashboard-plugins/${manifest.name}/${manifest.entry}`; const scriptSrc = import.meta.env.DEV ? `${baseUrl}?hermes_dv=${Date.now()}` : baseUrl; if (!import.meta.env.DEV) { if (loadedScripts.current.has(baseUrl)) continue; loadedScripts.current.add(baseUrl); }
const script = document.createElement("script"); script.setAttribute("data-hermes-plugin", manifest.name); script.src = scriptSrc; script.async = true; script.onerror = () => { setPluginLoadError(manifest.name, "LOAD_FAILED"); console.warn( `[plugins] Failed to load ${manifest.name} from ${scriptSrc} (open Network tab)`, ); }; script.onload = () => { notifyPluginRegistry(); queueMicrotask(() => { if (getPluginComponent(manifest.name)) return; setPluginLoadError(manifest.name, "NO_REGISTER"); }); }; document.body.appendChild(script); injectedScripts.push(script); }
// Give plugins a moment to load and register, then stop loading state. const timeout = setTimeout(() => setLoading(false), 2000); return () => { clearTimeout(timeout); if (import.meta.env.DEV) { for (const el of injectedScripts) { el.remove(); } } }; }, [manifests]);
// Listen for plugin registrations and resolve them against manifests. useEffect(() => { function resolvePlugins() { const resolved: RegisteredPlugin[] = []; for (const manifest of manifests) { const component = getPluginComponent(manifest.name); if (component) { resolved.push({ manifest, component }); } } setPlugins(resolved); // If all plugins registered, stop loading early. if (resolved.length === manifests.length && manifests.length > 0) { setLoading(false); } }
resolvePlugins(); const unsub = onPluginRegistered(resolvePlugins); return unsub; }, [manifests]);
return { plugins, manifests, loading };}