import { createElement, type ComponentType, } from "react"; import type { ExecutorFrontendPlugin, FrontendPluginRouteDefinition, } from "./types"; import { normalizeExecutorPluginPath, } from "./paths"; export type RegisteredFrontendPluginRoute = { plugin: ExecutorFrontendPlugin; route: FrontendPluginRouteDefinition; }; export const defineFrontendPluginRoute = < TDefinition extends FrontendPluginRouteDefinition, >( definition: TDefinition, ): TDefinition => definition; export const defineExecutorFrontendPlugin = < TPlugin extends ExecutorFrontendPlugin, >( input: TPlugin, ): TPlugin => input; // Plugin objects are registered once at module load, so route components need // to resolve lazily for React Fast Refresh to see edits inside plugin packages. export const liveFrontendPluginComponent = < TProps extends object, >( resolveComponent: () => ComponentType, ): ComponentType => { const resolvedComponent = resolveComponent(); const LiveFrontendPluginComponent = (props: TProps) => createElement(resolveComponent(), props); LiveFrontendPluginComponent.displayName = `LiveFrontendPluginComponent(${resolvedComponent.displayName ?? resolvedComponent.name ?? "Anonymous"})`; return LiveFrontendPluginComponent; }; export const registerExecutorFrontendPlugins = ( plugins: readonly ExecutorFrontendPlugin[], ) => { const pluginsByKey = new Map(); const routesByPluginAndKey = new Map(); const secretStoresByKind = new Map(); for (const plugin of plugins) { if (pluginsByKey.has(plugin.key)) { throw new Error(`Duplicate frontend plugin registration: ${plugin.key}`); } pluginsByKey.set(plugin.key, plugin); if (plugin.secretStore) { if (secretStoresByKind.has(plugin.secretStore.kind)) { throw new Error( `Duplicate frontend secret store registration: ${plugin.secretStore.kind}`, ); } secretStoresByKind.set(plugin.secretStore.kind, plugin); } const pluginRouteKeys = new Set(); const pluginRoutePaths = new Set(); for (const route of plugin.routes ?? []) { if (pluginRouteKeys.has(route.key)) { throw new Error( `Duplicate frontend plugin route key for ${plugin.key}: ${route.key}`, ); } pluginRouteKeys.add(route.key); const normalizedPath = normalizeExecutorPluginPath(route.path ?? ""); if (pluginRoutePaths.has(normalizedPath)) { throw new Error( `Duplicate frontend plugin route path for ${plugin.key}: ${normalizedPath || ""}`, ); } pluginRoutePaths.add(normalizedPath); routesByPluginAndKey.set(`${plugin.key}:${route.key}`, { plugin, route, }); } } const routes = [...routesByPluginAndKey.values()]; return { plugins: [...pluginsByKey.values()], routes, getPlugin: (key: string) => pluginsByKey.get(key) ?? null, getRoute: (pluginKey: string, routeKey: string) => routesByPluginAndKey.get(`${pluginKey}:${routeKey}`) ?? null, }; };