import { createFileRoute, Navigate, notFound } from "@tanstack/react-router"; import { useClientPlugins } from "@executor-js/sdk/client"; // --------------------------------------------------------------------------- // /plugins// // // Mounts pages contributed by client plugins. The host's // `` (set up at the root) materialises the // list of `ClientPluginSpec` from `virtual:executor/plugins-client`, // and this route reads it via `useClientPlugins()` — so adding a // plugin to `executor.config.ts` is sufficient for its pages to mount // here, with no per-route imports. // // Plugin pages use the same lightweight route vocabulary as the rest of the // console route tree: static segments and `$param` segments. The host route // owns that matching so plugin detail views can be real URLs instead of // in-component state. // --------------------------------------------------------------------------- export const Route = createFileRoute("/{-$orgSlug}/plugins/$pluginId/$")({ component: PluginRouteComponent, }); function normalizePath(input: string): string { if (!input || input === "/") return "/"; const withLeadingSlash = input.startsWith("/") ? input : `/${input}`; return withLeadingSlash.length > 1 ? withLeadingSlash.replace(/\/+$/, "") : "/"; } const pathSegments = (input: string): readonly string[] => normalizePath(input) .split("/") .filter((segment) => segment.length > 0); export const matchPluginPagePath = ( pattern: string, target: string, ): Readonly> | null => { const patternSegments = pathSegments(pattern); const targetSegments = pathSegments(target); if (patternSegments.length !== targetSegments.length) return null; const params: Record = {}; for (let index = 0; index < patternSegments.length; index += 1) { const patternSegment = patternSegments[index]!; const targetSegment = targetSegments[index]!; if (patternSegment.startsWith("$") && patternSegment.length > 1) { params[patternSegment.slice(1)] = decodeURIComponent(targetSegment); continue; } if (patternSegment !== targetSegment) return null; } return params; }; const matchScore = (pattern: string): number => pathSegments(pattern).reduce((score, segment) => score + (segment.startsWith("$") ? 1 : 2), 0); export const matchPluginPage = ( pages: readonly TPage[] | undefined, target: string, ): { readonly page: TPage; readonly params: Readonly> } | null => { const matches = pages ?.map((page, index) => ({ page, index, params: matchPluginPagePath(page.path, target), score: matchScore(page.path), })) .filter( ( candidate, ): candidate is { readonly page: TPage; readonly index: number; readonly params: Readonly>; readonly score: number; } => candidate.params !== null, ) .sort((a, b) => b.score - a.score || a.index - b.index) ?? []; const first = matches[0]; return first ? { page: first.page, params: first.params } : null; }; function PluginRouteComponent() { const { orgSlug, pluginId, _splat: rest } = Route.useParams(); const plugins = useClientPlugins(); const target = normalizePath(rest ?? "/"); if (pluginId === "toolkits") { const segments = pathSegments(target); const toolkitSlug = segments[0]; if (toolkitSlug) { return ( ); } return ; } const plugin = plugins.find((p) => p.id === pluginId); // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: TanStack Router represents not-found from components by throwing notFound() if (!plugin) throw notFound(); const match = matchPluginPage(plugin.pages, target); // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: TanStack Router represents not-found from components by throwing notFound() if (!match) throw notFound(); const Component = match.page.component; return ; }