* 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, HERMES_BASE_PATH } 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());
useEffect(() => {
api
.getPlugins()
.then((list) => {
setManifests(list);
if (list.length === 0) setLoading(false);
})
.catch(() => setLoading(false));
}, []);
useEffect(() => {
if (manifests.length === 0) return;
const injectedScripts: HTMLScriptElement[] = [];
for (const manifest of manifests) {
if (manifest.css) {
const cssUrl = `${HERMES_BASE_PATH}/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);
}
}
const baseUrl = `${HERMES_BASE_PATH}/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;
if (manifest.integrity && typeof manifest.integrity === "string") {
script.integrity = manifest.integrity;
script.crossOrigin = "anonymous";
}
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);
}
const timeout = setTimeout(() => setLoading(false), 2000);
return () => {
clearTimeout(timeout);
if (import.meta.env.DEV) {
for (const el of injectedScripts) {
el.remove();
}
}
};
}, [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 (resolved.length === manifests.length && manifests.length > 0) {
setLoading(false);
}
}
resolvePlugins();
const unsub = onPluginRegistered(resolvePlugins);
return unsub;
}, [manifests]);
return { plugins, manifests, loading };
}