From c064507bd34cca0dbd9e37c3bfd22749a214b05a Mon Sep 17 00:00:00 2001 From: CodexCoder Date: Fri, 22 May 2026 15:24:38 +0000 Subject: [PATCH] Register plugin UI exports eagerly Co-Authored-By: Paperclip --- ui/src/plugins/slots.test.ts | 29 +++++++++++++++++++++++++++++ ui/src/plugins/slots.tsx | 22 +++++++++++++++++++++- 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 ui/src/plugins/slots.test.ts diff --git a/ui/src/plugins/slots.test.ts b/ui/src/plugins/slots.test.ts new file mode 100644 index 00000000000..4b5b2fa7d60 --- /dev/null +++ b/ui/src/plugins/slots.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from "vitest"; +import { _collectRegisterableExportNamesForTests } from "./slots"; + +describe("plugin slot export registration", () => { + it("keeps declared missing exports visible for diagnostics", () => { + const exports = _collectRegisterableExportNamesForTests( + { Page: () => null }, + new Set(["Page", "MissingRouteSidebar"]), + ); + + expect([...exports]).toEqual(["Page", "MissingRouteSidebar"]); + }); + + it("registers component-like module exports even when the current contribution did not declare them", () => { + const exports = _collectRegisterableExportNamesForTests( + { + Page: () => null, + RouteSidebar: () => null, + webComponentTag: "paperclip-widget", + metadata: { ignored: true }, + count: 1, + default: () => null, + }, + new Set(["Page"]), + ); + + expect(exports).toEqual(new Set(["Page", "RouteSidebar", "webComponentTag"])); + }); +}); diff --git a/ui/src/plugins/slots.tsx b/ui/src/plugins/slots.tsx index ecc5f72d4aa..31e882c614b 100644 --- a/ui/src/plugins/slots.tsx +++ b/ui/src/plugins/slots.tsx @@ -177,6 +177,24 @@ export function resolveRegisteredPluginComponent( return registry.get(buildRegistryKey(pluginKey, exportName)) ?? null; } +function isRegisterablePluginExport(exported: unknown): boolean { + return typeof exported === "function" || typeof exported === "string"; +} + +function collectRegisterableExportNames( + mod: Record, + declaredExports: Set, +): Set { + const exportNames = new Set(declaredExports); + for (const [exportName, exported] of Object.entries(mod)) { + if (exportName === "default") continue; + if (isRegisterablePluginExport(exported)) { + exportNames.add(exportName); + } + } + return exportNames; +} + // --------------------------------------------------------------------------- // Plugin module dynamic import loader // --------------------------------------------------------------------------- @@ -471,7 +489,8 @@ async function loadPluginModule(contribution: PluginUiContribution): Promise