diff --git a/ui/src/plugins/bridge.test.ts b/ui/src/plugins/bridge.test.ts index 543cbcc91d0..b4a4fed272d 100644 --- a/ui/src/plugins/bridge.test.ts +++ b/ui/src/plugins/bridge.test.ts @@ -23,6 +23,7 @@ import { type PluginBridgeContextValue, } from "./bridge"; import { initPluginBridge } from "./bridge-init"; +import { _createReactShimSourceForTests } from "./slots"; function clickEvent( overrides: Partial> = {}, @@ -304,3 +305,20 @@ describe("plugin SDK markdown component bridge", () => { }))).toContain("Run lint"); }); }); + +describe("plugin React shim", () => { + it("re-exports every named export from the host React module", () => { + const source = _createReactShimSourceForTests(React); + + for (const name of Object.keys(React).sort()) { + if (name === "default") continue; + expect(source).toContain(`export const ${name} = R.${name};`); + } + + expect(source).toContain("export default R;"); + expect(source).toContain("export const useInsertionEffect = R.useInsertionEffect;"); + expect(source).toContain("export const useId = R.useId;"); + expect(source).toContain("export const useSyncExternalStore = R.useSyncExternalStore;"); + expect(source).toContain("export const startTransition = R.startTransition;"); + }); +}); diff --git a/ui/src/plugins/slots.tsx b/ui/src/plugins/slots.tsx index 80d1836ab2c..ecc5f72d4aa 100644 --- a/ui/src/plugins/slots.tsx +++ b/ui/src/plugins/slots.tsx @@ -29,6 +29,7 @@ import { type ReactNode, type ComponentType, } from "react"; +import * as ReactModule from "react"; import { useQuery } from "@tanstack/react-query"; import type { PluginLauncherDeclaration, @@ -244,24 +245,31 @@ function applyJsxRuntimeKey( return { ...(props ?? {}), key }; } +function createReactShimSource(reactModule: object): string { + const exportNames = Object.keys(reactModule) + .filter((name) => name !== "default" && /^[A-Za-z_$][\w$]*$/.test(name)) + .sort(); + const namedExports = exportNames + .map((name) => ` export const ${name} = R.${name};`) + .join("\n"); + + return ` + const R = globalThis.__paperclipPluginBridge__?.react; + if (!R) { + throw new Error("Paperclip plugin React runtime is not initialized."); + } + export default R; +${namedExports} + `; +} + function getShimBlobUrl(specifier: "react" | "react-dom" | "react-dom/client" | "react/jsx-runtime" | "sdk-ui"): string { if (shimBlobUrls[specifier]) return shimBlobUrls[specifier]; let source: string; switch (specifier) { case "react": - source = ` - const R = globalThis.__paperclipPluginBridge__?.react; - export default R; - const { useState, useEffect, useCallback, useMemo, useRef, useContext, - createContext, createElement, Fragment, Component, forwardRef, - memo, lazy, Suspense, StrictMode, cloneElement, Children, - isValidElement, createRef } = R; - export { useState, useEffect, useCallback, useMemo, useRef, useContext, - createContext, createElement, Fragment, Component, forwardRef, - memo, lazy, Suspense, StrictMode, cloneElement, Children, - isValidElement, createRef }; - `; + source = createReactShimSource(ReactModule); break; case "react/jsx-runtime": source = ` @@ -900,4 +908,5 @@ export function _resetPluginModuleLoader(): void { } export const _applyJsxRuntimeKeyForTests = applyJsxRuntimeKey; +export const _createReactShimSourceForTests = createReactShimSource; export const _rewriteBareSpecifiersForTests = rewriteBareSpecifiers;