import type { Renderers, ToolRenderer } from "@berry/api/web.ts"; /** An extension's frontend as the server built it: a module to import, and its styles. */ export type WebEntry = { module: string; styles: string[] }; const loaded = new Map< string, { module: string; renderers: Renderers | null; links: HTMLLinkElement[] } >(); /** Extensions by the names they went by before, which old history still calls them. */ const formerly = new Map(); /** * Bring the extensions' frontends to what the server built: import the ones whose module is new, and drop the ones * gone. Says whether anything changed, for views to draw again with the new renderers. */ export async function syncRenderers( extensions: readonly { name: string; web: WebEntry | null; formerly: readonly string[] }[], ) { let changed = false; formerly.clear(); for (const { name, formerly: names } of extensions) { for (const former of names) formerly.set(former, name); } const named = new Map(extensions.map((extension) => [extension.name, extension.web])); for (const [name, entry] of loaded) { if (named.has(name)) continue; for (const link of entry.links) link.remove(); loaded.delete(name); changed = true; } await Promise.all([...named].map(async ([name, web]) => { const current = loaded.get(name); const stylesheets = (hrefs: readonly string[]) => hrefs.map((href) => { const link = document.createElement("link"); link.rel = "stylesheet"; link.href = href; document.head.append(link); return link; }); if ((current?.module ?? null) === (web?.module ?? null)) { // a module's url follows its scripts, so a change only to its styles keeps it const hrefs = current?.links.map((link) => link.getAttribute("href")); if (current && web && hrefs!.join("\n") !== web.styles.join("\n")) { for (const link of current.links) link.remove(); current.links = stylesheets(web.styles); } return; } changed = true; for (const link of current?.links ?? []) link.remove(); if (web === null) return void loaded.delete(name); const links = stylesheets(web.styles); let renderers: Renderers | null = null; try { renderers = (await import(/* @vite-ignore */ web.module)).default; } catch (error) { console.error(`[berry] ${name}'s frontend:`, error); } loaded.set(name, { module: web.module, renderers, links }); })); return changed; } export function extensionRenderers(extension: string): Renderers | undefined { return (loaded.get(extension) ?? loaded.get(formerly.get(extension) ?? ""))?.renderers ?? undefined; } /** A script's call names its extension; the model's direct calls do not, so any extension's renderer of that name. */ export function toolRenderer(name: string, extension?: string): ToolRenderer | undefined { if (extension !== undefined) return extensionRenderers(extension)?.tools?.[name]; for (const { renderers } of loaded.values()) { const found = renderers?.tools?.[name]; if (found) return found; } return undefined; }