diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 03fb338174..a5f80abd91 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -13,6 +13,7 @@ import { NotificationClearAll } from "./components/notification/NotificationClea import { NotificationIdLine } from "./components/notification/NotificationIdLine.jsx"; import { PageTitle } from "./components/widgets/page-title/PageTitle.jsx"; import { THEME } from "./helpers/GetStaticData.js"; +import { loadPlugin } from "./helpers/pluginLoader.js"; import { attachRequestIdInterceptor } from "./helpers/requestId.js"; import PostHogPageviewTracker from "./PostHogPageviewTracker.js"; import { Router } from "./routes/Router.jsx"; @@ -26,15 +27,11 @@ if (!axios[GLOBAL_INTERCEPTOR_FLAG]) { axios[GLOBAL_INTERCEPTOR_FLAG] = true; } -let GoogleTagManagerHelper; -try { - const mod = await import( - "./plugins/google-tag-manager-helper/GoogleTagManagerHelper.js" - ); - GoogleTagManagerHelper = mod.GoogleTagManagerHelper; -} catch { - // The component will remain null of it is not available -} +const GoogleTagManagerHelper = await loadPlugin(() => + import("./plugins/google-tag-manager-helper/GoogleTagManagerHelper.js").then( + (m) => m.GoogleTagManagerHelper, + ), +); function App() { const { sessionDetails, isLogoutLoading } = useSessionStore(); diff --git a/frontend/src/components/agency/configure-connector-modal/ConfigureConnectorModal.jsx b/frontend/src/components/agency/configure-connector-modal/ConfigureConnectorModal.jsx index 3df2ac526c..40477a1133 100644 --- a/frontend/src/components/agency/configure-connector-modal/ConfigureConnectorModal.jsx +++ b/frontend/src/components/agency/configure-connector-modal/ConfigureConnectorModal.jsx @@ -11,6 +11,7 @@ import { Tabs } from "@/components/ui/shims/antd-structure"; import { Typography } from "@/components/ui/shims/antd-typography"; import { fetchAllPages } from "../../../helpers/pagination"; +import { loadPlugin } from "../../../helpers/pluginLoader.js"; import { useAxiosPrivate } from "../../../hooks/useAxiosPrivate"; import { useExceptionHandler } from "../../../hooks/useExceptionHandler"; import usePostHogEvents from "../../../hooks/usePostHogEvents"; @@ -24,18 +25,10 @@ import { ReadOnlyNotice } from "../../widgets/read-only-notice/ReadOnlyNotice"; import { ConfigureFormsLayout } from "../configure-forms-layout/ConfigureFormsLayout"; import "./ConfigureConnectorModal.css"; -let RuleEngine; -let ruleEngineTabs; - -try { - const ruleEnginePlugin = await import( - "../../../plugins/manual-review/rule-engine" - ); - RuleEngine = ruleEnginePlugin.RuleEngine; - ruleEngineTabs = ruleEnginePlugin.ruleEngineTabs; -} catch { - // Plugin not available -} +const { RuleEngine, ruleEngineTabs } = await loadPlugin( + () => import("../../../plugins/manual-review/rule-engine"), + {}, +); function ConfigureConnectorModal({ open, diff --git a/frontend/src/components/agency/ds-settings-card/DsSettingsCard.jsx b/frontend/src/components/agency/ds-settings-card/DsSettingsCard.jsx index 2ca1c4d083..7f40ea176e 100644 --- a/frontend/src/components/agency/ds-settings-card/DsSettingsCard.jsx +++ b/frontend/src/components/agency/ds-settings-card/DsSettingsCard.jsx @@ -7,6 +7,7 @@ import { Image } from "@/components/ui/shims/antd-leaves"; import { Tooltip } from "@/components/ui/shims/antd-overlays"; import { Typography } from "@/components/ui/shims/antd-typography"; +import { loadPlugin } from "../../../helpers/pluginLoader.js"; import { useAxiosPrivate } from "../../../hooks/useAxiosPrivate"; import { useExceptionHandler } from "../../../hooks/useExceptionHandler"; import useRequestUrl from "../../../hooks/useRequestUrl"; @@ -67,19 +68,15 @@ function DsSettingsCard({ connType, endpointDetails, message }) { }; useEffect(() => { - const loadPlugin = async () => { - try { - const mod = await import( - "../../../plugins/dscard-input-options/AppDeploymentCardInputOptions" - ); - if (flags.app_deployment && mod.appDeploymentInputOption) { - setUpdatedInputoptions(mod.appDeploymentInputOption); - } - } catch { - // The component will remain null of it is not available + loadPlugin(() => + import( + "../../../plugins/dscard-input-options/AppDeploymentCardInputOptions" + ).then((m) => m.appDeploymentInputOption), + ).then((appDeploymentInputOption) => { + if (flags.app_deployment && appDeploymentInputOption) { + setUpdatedInputoptions(appDeploymentInputOption); } - }; - loadPlugin(); + }); }, []); useEffect(() => { diff --git a/frontend/src/components/custom-tools/combined-output/CombinedOutput.jsx b/frontend/src/components/custom-tools/combined-output/CombinedOutput.jsx index 776b9eea59..cc385aa859 100644 --- a/frontend/src/components/custom-tools/combined-output/CombinedOutput.jsx +++ b/frontend/src/components/custom-tools/combined-output/CombinedOutput.jsx @@ -11,6 +11,7 @@ import { displayPromptResult, promptType, } from "../../../helpers/GetStaticData"; +import { loadPlugin } from "../../../helpers/pluginLoader.js"; import { useAxiosPrivate } from "../../../hooks/useAxiosPrivate"; import { useAlertStore } from "../../../store/alert-store"; import { useCustomToolStore } from "../../../store/custom-tool-store"; @@ -19,41 +20,34 @@ import "./CombinedOutput.css"; import { useExceptionHandler } from "../../../hooks/useExceptionHandler"; import { JsonView } from "./JsonView"; -let TableView; -let promptOutputApiSps; -try { - const tvMod = await import("../../../plugins/simple-prompt-studio/TableView"); - TableView = tvMod.TableView; - const helperMod = await import( - "../../../plugins/simple-prompt-studio/helper" - ); - promptOutputApiSps = helperMod.promptOutputApiSps; -} catch { - // The component will remain null if it is not available -} +const TableView = await loadPlugin(() => + import("../../../plugins/simple-prompt-studio/TableView").then( + (m) => m.TableView, + ), +); +const promptOutputApiSps = await loadPlugin(() => + import("../../../plugins/simple-prompt-studio/helper").then( + (m) => m.promptOutputApiSps, + ), +); -let publicOutputsApi; -let publicDefaultOutputApi; -try { - const mod = await import( - "../../../plugins/prompt-studio-public-share/helpers/PublicShareAPIs" - ); - publicOutputsApi = mod.publicOutputsApi; - publicDefaultOutputApi = mod.publicDefaultOutputApi; -} catch { - // The component will remain null if it is not available -} +const { publicOutputsApi, publicDefaultOutputApi } = await loadPlugin( + () => + import( + "../../../plugins/prompt-studio-public-share/helpers/PublicShareAPIs" + ), + {}, +); // OSS falls back to passthrough helpers — no enrichment. -let splitCombinedData = (data) => ({ combined: data, bundle: null }); -let buildEnrichedFromBundle = (_output, _bundle, _formatter) => ({}); -let getEnrichmentFromItem = (_item) => null; -try { - const mod = await import("../../../plugins/lookup-enriched-toggle/helpers"); - splitCombinedData = mod.splitCombinedData; - buildEnrichedFromBundle = mod.buildEnrichedFromBundle; - getEnrichmentFromItem = mod.getEnrichmentFromItem; -} catch {} +const { + splitCombinedData = (data) => ({ combined: data, bundle: null }), + buildEnrichedFromBundle = (_output, _bundle, _formatter) => ({}), + getEnrichmentFromItem = (_item) => null, +} = await loadPlugin( + () => import("../../../plugins/lookup-enriched-toggle/helpers"), + {}, +); const buildDefaultProfileOutputs = (data) => { const { combined: payload, bundle } = splitCombinedData(data); diff --git a/frontend/src/components/custom-tools/combined-output/JsonView.jsx b/frontend/src/components/custom-tools/combined-output/JsonView.jsx index f05dfd2638..ce1ea3bb87 100644 --- a/frontend/src/components/custom-tools/combined-output/JsonView.jsx +++ b/frontend/src/components/custom-tools/combined-output/JsonView.jsx @@ -4,18 +4,15 @@ import { useEffect, useRef, useState } from "react"; import { Tooltip } from "@/components/ui/shims/antd-overlays"; import { Tabs } from "@/components/ui/shims/antd-structure"; +import { loadPlugin } from "../../../helpers/pluginLoader.js"; import { useCustomToolStore } from "../../../store/custom-tool-store"; import { JsonViewBody } from "./JsonViewBody"; -let EnrichedOutputToggle; -try { - const mod = await import( - "../../../plugins/lookup-enriched-toggle/EnrichedOutputToggle" - ); - EnrichedOutputToggle = mod.EnrichedOutputToggle; -} catch { - // The component will remain undefined if it is not available -} +const EnrichedOutputToggle = await loadPlugin(() => + import("../../../plugins/lookup-enriched-toggle/EnrichedOutputToggle").then( + (m) => m.EnrichedOutputToggle, + ), +); function JsonView({ combinedOutput, diff --git a/frontend/src/components/custom-tools/document-manager/DocumentManager.jsx b/frontend/src/components/custom-tools/document-manager/DocumentManager.jsx index e1cc1b14d0..d30d2f6ef5 100644 --- a/frontend/src/components/custom-tools/document-manager/DocumentManager.jsx +++ b/frontend/src/components/custom-tools/document-manager/DocumentManager.jsx @@ -17,6 +17,7 @@ import { base64toBlobWithMime, docIndexStatus, } from "../../../helpers/GetStaticData"; +import { loadPlugin } from "../../../helpers/pluginLoader.js"; import { useAxiosPrivate } from "../../../hooks/useAxiosPrivate"; import usePostHogEvents from "../../../hooks/usePostHogEvents"; import { useCustomToolStore } from "../../../store/custom-tool-store"; @@ -44,41 +45,34 @@ const viewTypes = { }; // Import components for the summarize feature -let SummarizeView = null; -try { - const svMod = await import("../../../plugins/summarize-view/SummarizeView"); - SummarizeView = svMod.SummarizeView; - const stMod = await import("../../../plugins/summarize-tab/SummarizeTab"); - const tabLabel = stMod.tabLabel; - if (tabLabel) { - items.push({ - key: "3", - label: tabLabel, - }); - } -} catch { - // The component will remain null of it is not available +const SummarizeView = await loadPlugin(() => + import("../../../plugins/summarize-view/SummarizeView").then( + (m) => m.SummarizeView, + ), +); +const summarizeTabLabel = await loadPlugin(() => + import("../../../plugins/summarize-tab/SummarizeTab").then((m) => m.tabLabel), +); +// The tab is only useful when the view it opens is available too. +if (SummarizeView && summarizeTabLabel) { + items.push({ + key: "3", + label: summarizeTabLabel, + }); } // Import component for the simple prompt studio feature -let getDocumentsSps; -try { - const mod = await import( +const getDocumentsSps = await loadPlugin(() => + import( "../../../plugins/simple-prompt-studio/simple-prompt-studio-api-service" - ); - getDocumentsSps = mod.getDocumentsSps; -} catch { - // The component will remain null of it is not available -} -let publicDocumentApi; -try { - const mod = await import( + ).then((m) => m.getDocumentsSps), +); +const publicDocumentApi = await loadPlugin(() => + import( "../../../plugins/prompt-studio-public-share/helpers/PublicShareAPIs" - ); - publicDocumentApi = mod.publicDocumentApi; -} catch { - // The component will remain null of it is not available -} + ).then((m) => m.publicDocumentApi), +); + function DocumentManager({ generateIndex, handleUpdateTool, handleDocChange }) { const [openManageDocsModal, setOpenManageDocsModal] = useState(false); const [page, setPage] = useState(1); diff --git a/frontend/src/components/custom-tools/document-parser/DocumentParser.jsx b/frontend/src/components/custom-tools/document-parser/DocumentParser.jsx index c7d9fd1004..f34d31335c 100644 --- a/frontend/src/components/custom-tools/document-parser/DocumentParser.jsx +++ b/frontend/src/components/custom-tools/document-parser/DocumentParser.jsx @@ -4,6 +4,7 @@ import { useSearchParams } from "react-router-dom"; import "./DocumentParser.css"; import { promptType } from "../../../helpers/GetStaticData"; +import { loadPlugin } from "../../../helpers/pluginLoader.js"; import { useAxiosPrivate } from "../../../hooks/useAxiosPrivate"; import { useExceptionHandler } from "../../../hooks/useExceptionHandler"; import { useAlertStore } from "../../../store/alert-store"; @@ -13,25 +14,21 @@ import { useSessionStore } from "../../../store/session-store"; import { EmptyState } from "../../widgets/empty-state/EmptyState"; import { PromptCardWrapper } from "../prompt-card/PromptCardWrapper"; -let promptCardService; -let promptPatchApiSps; -let SpsPromptsEmptyState; -try { - const pcMod = await import( - "../../../plugins/prompt-card/prompt-card-service" - ); - promptCardService = pcMod.promptCardService; - const helperMod = await import( - "../../../plugins/simple-prompt-studio/helper" - ); - promptPatchApiSps = helperMod.promptPatchApiSps; - const spsMod = await import( - "../../../plugins/simple-prompt-studio/SpsPromptsEmptyState" - ); - SpsPromptsEmptyState = spsMod.SpsPromptsEmptyState; -} catch { - // The component will remain null of it is not available -} +const promptCardService = await loadPlugin(() => + import("../../../plugins/prompt-card/prompt-card-service").then( + (m) => m.promptCardService, + ), +); +const promptPatchApiSps = await loadPlugin(() => + import("../../../plugins/simple-prompt-studio/helper").then( + (m) => m.promptPatchApiSps, + ), +); +const SpsPromptsEmptyState = await loadPlugin(() => + import("../../../plugins/simple-prompt-studio/SpsPromptsEmptyState").then( + (m) => m.SpsPromptsEmptyState, + ), +); // Module-scoped to avoid per-render recompilation. const UUID_RE = diff --git a/frontend/src/components/custom-tools/header/Header.jsx b/frontend/src/components/custom-tools/header/Header.jsx index bae4bbdc4a..174a0aea1e 100644 --- a/frontend/src/components/custom-tools/header/Header.jsx +++ b/frontend/src/components/custom-tools/header/Header.jsx @@ -6,6 +6,7 @@ import { Form } from "@/components/ui/shims/antd-form"; import { Input } from "@/components/ui/shims/antd-inputs"; import { Dropdown, Modal, Tooltip } from "@/components/ui/shims/antd-overlays"; import { ExportToolIcon } from "../../../assets"; +import { loadPlugin } from "../../../helpers/pluginLoader.js"; import { useAxiosPrivate } from "../../../hooks/useAxiosPrivate"; import { useExceptionHandler } from "../../../hooks/useExceptionHandler"; import usePostHogEvents from "../../../hooks/usePostHogEvents"; @@ -19,27 +20,21 @@ import { CustomButton } from "../../widgets/custom-button/CustomButton"; import { ExportTool } from "../export-tool/ExportTool"; import "./Header.css"; -let SinglePassToggleSwitch; -let CloneButton; -let PromptShareButton; -try { - const mod = await import( +const SinglePassToggleSwitch = await loadPlugin(() => + import( "../../../plugins/single-pass-toggle-switch/SinglePassToggleSwitch" - ); - SinglePassToggleSwitch = mod.SinglePassToggleSwitch; -} catch {} -try { - const mod = await import( + ).then((m) => m.SinglePassToggleSwitch), +); +const PromptShareButton = await loadPlugin(() => + import( "../../../plugins/prompt-studio-public-share/public-share-btn/PromptShareButton.jsx" - ); - PromptShareButton = mod.PromptShareButton; -} catch {} -try { - const mod = await import( - "../../../plugins/prompt-studio-clone/clone-btn/CloneButton.jsx" - ); - CloneButton = mod.CloneButton; -} catch {} + ).then((m) => m.PromptShareButton), +); +const CloneButton = await loadPlugin(() => + import("../../../plugins/prompt-studio-clone/clone-btn/CloneButton.jsx").then( + (m) => m.CloneButton, + ), +); const noopCheckLookups = () => Promise.resolve(true); diff --git a/frontend/src/components/custom-tools/manage-docs-modal/ManageDocsModal.jsx b/frontend/src/components/custom-tools/manage-docs-modal/ManageDocsModal.jsx index 5123867484..8f712aab75 100644 --- a/frontend/src/components/custom-tools/manage-docs-modal/ManageDocsModal.jsx +++ b/frontend/src/components/custom-tools/manage-docs-modal/ManageDocsModal.jsx @@ -10,6 +10,7 @@ import { Modal, Tooltip } from "@/components/ui/shims/antd-overlays"; import { Table, Upload } from "@/components/ui/shims/antd-structure"; import { Typography } from "@/components/ui/shims/antd-typography"; +import { loadPlugin } from "../../../helpers/pluginLoader.js"; import { useAxiosPrivate } from "../../../hooks/useAxiosPrivate"; import { useExceptionHandler } from "../../../hooks/useExceptionHandler"; import { useAlertStore } from "../../../store/alert-store"; @@ -24,35 +25,23 @@ import "./ManageDocsModal.css"; import usePostHogEvents from "../../../hooks/usePostHogEvents"; import { usePromptOutputStore } from "../../../store/prompt-output-store"; -let SummarizeStatusTitle = null; -try { - const mod = await import( - "../../../plugins/summarize-status-title/SummarizeStatusTitle" - ); - SummarizeStatusTitle = mod.SummarizeStatusTitle; -} catch { - // The component will remain null if it is not available -} +const SummarizeStatusTitle = await loadPlugin(() => + import("../../../plugins/summarize-status-title/SummarizeStatusTitle").then( + (m) => m.SummarizeStatusTitle, + ), +); -let publicIndexApi = null; -try { - const mod = await import( +const publicIndexApi = await loadPlugin(() => + import( "../../../plugins/prompt-studio-public-share/helpers/PublicShareAPIs" - ); - publicIndexApi = mod.publicIndexApi; -} catch { - // The component will remain null if it is not available -} - -let ConfirmMultiDoc = null; -try { - const mod = await import( - "../../../plugins/prompt-studio-multi-doc/ConfirmMultiDoc" - ); - ConfirmMultiDoc = mod.ConfirmMultiDoc; -} catch { - // The component will remain null if it is not available -} + ).then((m) => m.publicIndexApi), +); + +const ConfirmMultiDoc = await loadPlugin(() => + import("../../../plugins/prompt-studio-multi-doc/ConfirmMultiDoc").then( + (m) => m.ConfirmMultiDoc, + ), +); const indexTypes = { raw: "RAW", diff --git a/frontend/src/components/custom-tools/output-analyzer/OutputAnalyzerCard.jsx b/frontend/src/components/custom-tools/output-analyzer/OutputAnalyzerCard.jsx index 2625fbf2ea..3e3d4d99d4 100644 --- a/frontend/src/components/custom-tools/output-analyzer/OutputAnalyzerCard.jsx +++ b/frontend/src/components/custom-tools/output-analyzer/OutputAnalyzerCard.jsx @@ -6,6 +6,7 @@ import { Divider } from "@/components/ui/shims/antd-leaves"; import { Typography } from "@/components/ui/shims/antd-typography"; import { base64toBlob } from "../../../helpers/GetStaticData"; +import { loadPlugin } from "../../../helpers/pluginLoader.js"; import { useAxiosPrivate } from "../../../hooks/useAxiosPrivate"; import { useExceptionHandler } from "../../../hooks/useExceptionHandler"; import { useAlertStore } from "../../../store/alert-store"; @@ -15,15 +16,11 @@ import { CombinedOutput } from "../combined-output/CombinedOutput"; import { DocumentViewer } from "../document-viewer/DocumentViewer"; import { PdfViewer } from "../pdf-viewer/PdfViewer"; -let publicDocumentApi; -try { - const mod = await import( +const publicDocumentApi = await loadPlugin(() => + import( "../../../plugins/prompt-studio-public-share/helpers/PublicShareAPIs" - ); - publicDocumentApi = mod.publicDocumentApi; -} catch { - // The component will remain null if it is not available -} + ).then((m) => m.publicDocumentApi), +); function OutputAnalyzerCard({ doc, selectedPrompts, totalFields }) { const [fileUrl, setFileUrl] = useState(""); diff --git a/frontend/src/components/custom-tools/output-analyzer/OutputAnalyzerHeader.jsx b/frontend/src/components/custom-tools/output-analyzer/OutputAnalyzerHeader.jsx index 32ffab41f1..18027d8053 100644 --- a/frontend/src/components/custom-tools/output-analyzer/OutputAnalyzerHeader.jsx +++ b/frontend/src/components/custom-tools/output-analyzer/OutputAnalyzerHeader.jsx @@ -12,18 +12,15 @@ import { Button } from "@/components/ui/shims/antd-button"; import { Space } from "@/components/ui/shims/antd-layout"; import { Drawer, Menu } from "@/components/ui/shims/antd-structure"; import { Typography } from "@/components/ui/shims/antd-typography"; +import { loadPlugin } from "../../../helpers/pluginLoader.js"; import { useCustomToolStore } from "../../../store/custom-tool-store"; import { useSessionStore } from "../../../store/session-store"; -let HeaderPublic; -try { - const mod = await import( +const HeaderPublic = await loadPlugin(() => + import( "../../../plugins/prompt-studio-public-share/header-public/HeaderPublic.jsx" - ); - HeaderPublic = mod.HeaderPublic; -} catch { - // Do nothing if plugins are not loaded. -} + ).then((m) => m.HeaderPublic), +); const PAGINATION_ACTIONS = { PREV: "PREV", diff --git a/frontend/src/components/custom-tools/output-for-doc-modal/OutputForDocModal.jsx b/frontend/src/components/custom-tools/output-for-doc-modal/OutputForDocModal.jsx index 37449e2dea..475e37f218 100644 --- a/frontend/src/components/custom-tools/output-for-doc-modal/OutputForDocModal.jsx +++ b/frontend/src/components/custom-tools/output-for-doc-modal/OutputForDocModal.jsx @@ -14,21 +14,18 @@ import { displayPromptResult, getDocIdFromKey, } from "../../../helpers/GetStaticData"; +import { loadPlugin } from "../../../helpers/pluginLoader.js"; import { useExceptionHandler } from "../../../hooks/useExceptionHandler"; import { useAlertStore } from "../../../store/alert-store"; import { useTokenUsageStore } from "../../../store/token-usage-store"; import { SpinnerLoader } from "../../widgets/spinner-loader/SpinnerLoader"; import { ProfileInfoBar } from "../profile-info-bar/ProfileInfoBar"; -let publicOutputsApi; -try { - const mod = await import( +const publicOutputsApi = await loadPlugin(() => + import( "../../../plugins/prompt-studio-public-share/helpers/PublicShareAPIs" - ); - publicOutputsApi = mod.publicOutputsApi; -} catch { - // The component will remain null of it is not available -} + ).then((m) => m.publicOutputsApi), +); const outputStatus = { yet_to_process: "YET_TO_PROCESS", diff --git a/frontend/src/components/custom-tools/pdf-viewer/PdfViewer.jsx b/frontend/src/components/custom-tools/pdf-viewer/PdfViewer.jsx index af3c349a11..78ba1d0cde 100644 --- a/frontend/src/components/custom-tools/pdf-viewer/PdfViewer.jsx +++ b/frontend/src/components/custom-tools/pdf-viewer/PdfViewer.jsx @@ -13,14 +13,13 @@ import "@react-pdf-viewer/default-layout/lib/styles/index.css"; import "@react-pdf-viewer/highlight/lib/styles/index.css"; import "./Highlight.css"; import { PDF_WORKER_URL } from "../../../helpers/pdfWorkerConfig"; +import { loadPlugin } from "../../../helpers/pluginLoader.js"; -let RenderHighlights; -try { - const mod = await import("../../../plugins/pdf-highlight/RenderHighlights"); - RenderHighlights = mod.RenderHighlights; -} catch { - // Do nothing, no plugin will be loaded. -} +const RenderHighlights = await loadPlugin(() => + import("../../../plugins/pdf-highlight/RenderHighlights").then( + (m) => m.RenderHighlights, + ), +); function PdfLoadError({ error, onRetry, reportError }) { useEffect(() => { diff --git a/frontend/src/components/custom-tools/prompt-card/Header.jsx b/frontend/src/components/custom-tools/prompt-card/Header.jsx index 7690c69844..739dfbb6d7 100644 --- a/frontend/src/components/custom-tools/prompt-card/Header.jsx +++ b/frontend/src/components/custom-tools/prompt-card/Header.jsx @@ -21,56 +21,42 @@ import { PROMPT_RUN_TYPES, promptStudioUpdateStatus, } from "../../../helpers/GetStaticData"; +import { loadPlugin } from "../../../helpers/pluginLoader.js"; import { useCustomToolStore } from "../../../store/custom-tool-store"; import { ConfirmModal } from "../../widgets/confirm-modal/ConfirmModal"; import { EditableText } from "../editable-text/EditableText"; import { ExpandCardBtn } from "./ExpandCardBtn"; -let PromptRunBtnSps; -try { - const mod = await import( - "../../../plugins/simple-prompt-studio/PromptRunBtnSps" - ); - PromptRunBtnSps = mod.PromptRunBtnSps; -} catch { - // The component will remain 'undefined' it is not available -} +const PromptRunBtnSps = await loadPlugin(() => + import("../../../plugins/simple-prompt-studio/PromptRunBtnSps").then( + (m) => m.PromptRunBtnSps, + ), +); -let PromptChangeIndicator; -try { - const mod = await import( - "../../../plugins/prompt-change-indicator/PromptChangeIndicator" - ); - PromptChangeIndicator = mod.PromptChangeIndicator; -} catch { - // Cloud-only feedback loop indicator; stays undefined in OSS builds -} +// Cloud-only feedback loop indicator; stays null in OSS builds +const PromptChangeIndicator = await loadPlugin(() => + import("../../../plugins/prompt-change-indicator/PromptChangeIndicator").then( + (m) => m.PromptChangeIndicator, + ), +); -let LookupMenuItem; -try { - const mod = await import( - "../../../plugins/lookup-studio/prompt-card/LookupMenuItem" - ); - LookupMenuItem = mod.LookupMenuItem; -} catch {} +const LookupMenuItem = await loadPlugin(() => + import("../../../plugins/lookup-studio/prompt-card/LookupMenuItem").then( + (m) => m.LookupMenuItem, + ), +); -let usePromptRunGatePlugin; -let lookupPluginLoadError; -try { - const mod = await import( - "../../../plugins/lookup-studio/hooks/usePromptRunGate" - ); - usePromptRunGatePlugin = mod.usePromptRunGate; -} catch (err) { - lookupPluginLoadError = err; -} +const usePromptRunGatePlugin = await loadPlugin(() => + import("../../../plugins/lookup-studio/hooks/usePromptRunGate").then( + (m) => m.usePromptRunGate, + ), +); // Sibling plugin loaded but this hook didn't — surface so the no-op fallback // doesn't silently disable the run gate. -if (lookupPluginLoadError && LookupMenuItem) { +if (!usePromptRunGatePlugin && LookupMenuItem) { console.warn( - "[Header] lookup-studio plugin loaded but usePromptRunGate failed to import", - lookupPluginLoadError, + "[Header] lookup-studio plugin loaded but usePromptRunGate is unavailable", ); } diff --git a/frontend/src/components/custom-tools/prompt-card/PromptCard.jsx b/frontend/src/components/custom-tools/prompt-card/PromptCard.jsx index cbc9e9590f..f0aafddbd0 100644 --- a/frontend/src/components/custom-tools/prompt-card/PromptCard.jsx +++ b/frontend/src/components/custom-tools/prompt-card/PromptCard.jsx @@ -5,6 +5,7 @@ import { PROMPT_RUN_API_STATUSES, promptStudioUpdateStatus, } from "../../../helpers/GetStaticData"; +import { loadPlugin } from "../../../helpers/pluginLoader.js"; import usePostHogEvents from "../../../hooks/usePostHogEvents"; import { useAlertStore } from "../../../store/alert-store"; import { useCustomToolStore } from "../../../store/custom-tool-store"; @@ -14,18 +15,14 @@ import { PromptCardItems } from "./PromptCardItems"; import "./PromptCard.css"; import { handleUpdateStatus } from "./constants"; -let useEnforceTypeSwitchGatePlugin; -try { - const mod = await import( - "../../../plugins/lookup-studio/hooks/useEnforceTypeSwitchGate" - ); - useEnforceTypeSwitchGatePlugin = mod.useEnforceTypeSwitchGate; -} catch { - // Cloud plugin not present; gate falls back to no-op below. -} - -const useEnforceTypeSwitchGate = - useEnforceTypeSwitchGatePlugin || (() => () => null); +// Cloud plugin; without it the gate falls back to a no-op. +const useEnforceTypeSwitchGate = await loadPlugin( + () => + import( + "../../../plugins/lookup-studio/hooks/useEnforceTypeSwitchGate" + ).then((m) => m.useEnforceTypeSwitchGate), + () => () => null, +); // Fields with an inline error renderer on the card. Anything else falls back // to the global alert, so widening the sanitizer cannot silence a rejection. diff --git a/frontend/src/components/custom-tools/prompt-card/PromptCardItems.jsx b/frontend/src/components/custom-tools/prompt-card/PromptCardItems.jsx index 699f68f709..0225eecb47 100644 --- a/frontend/src/components/custom-tools/prompt-card/PromptCardItems.jsx +++ b/frontend/src/components/custom-tools/prompt-card/PromptCardItems.jsx @@ -8,6 +8,7 @@ import { Divider, Tag } from "@/components/ui/shims/antd-leaves"; import { Collapse } from "@/components/ui/shims/antd-overlays"; import { Card } from "@/components/ui/shims/antd-structure"; import { Typography } from "@/components/ui/shims/antd-typography"; +import { loadPlugin } from "../../../helpers/pluginLoader.js"; import { useCustomToolStore } from "../../../store/custom-tool-store"; import { SpinnerLoader } from "../../widgets/spinner-loader/SpinnerLoader"; import { EditableText } from "../editable-text/EditableText"; @@ -15,33 +16,23 @@ import { Header } from "./Header"; import { OutputForIndex } from "./OutputForIndex"; import { PromptOutput } from "./PromptOutput"; -let TableExtractionSettingsBtn; -try { - const mod = await import( - "../../../plugins/prompt-card/TableExtractionSettingsBtn" - ); - TableExtractionSettingsBtn = mod.TableExtractionSettingsBtn; -} catch { - // The component will remain null of it is not available -} +const TableExtractionSettingsBtn = await loadPlugin(() => + import("../../../plugins/prompt-card/TableExtractionSettingsBtn").then( + (m) => m.TableExtractionSettingsBtn, + ), +); -let LookupIndicator; -try { - const mod = await import( - "../../../plugins/lookup-studio/prompt-card/LookupIndicator" - ); - LookupIndicator = mod.LookupIndicator; -} catch {} +const LookupIndicator = await loadPlugin(() => + import("../../../plugins/lookup-studio/prompt-card/LookupIndicator").then( + (m) => m.LookupIndicator, + ), +); -let AgenticTableChecklist; -try { - const mod = await import( - "../../../plugins/prompt-card/AgenticTableChecklist" - ); - AgenticTableChecklist = mod.AgenticTableChecklist; -} catch { - // The component will remain null of it is not available -} +const AgenticTableChecklist = await loadPlugin(() => + import("../../../plugins/prompt-card/AgenticTableChecklist").then( + (m) => m.AgenticTableChecklist, + ), +); function PromptCardItems({ promptDetails, diff --git a/frontend/src/components/custom-tools/prompt-card/PromptOutput.jsx b/frontend/src/components/custom-tools/prompt-card/PromptOutput.jsx index 738e1832b4..fbc9ee7b7c 100644 --- a/frontend/src/components/custom-tools/prompt-card/PromptOutput.jsx +++ b/frontend/src/components/custom-tools/prompt-card/PromptOutput.jsx @@ -15,6 +15,7 @@ import { PROMPT_RUN_API_STATUSES, PROMPT_RUN_TYPES, } from "../../../helpers/GetStaticData"; +import { loadPlugin } from "../../../helpers/pluginLoader.js"; import usePromptOutput from "../../../hooks/usePromptOutput"; import { useWindowDimensions } from "../../../hooks/useWindowDimensions"; import { useAlertStore } from "../../../store/alert-store"; @@ -28,40 +29,28 @@ import { PromptOutputExpandBtn } from "./PromptOutputExpandBtn"; import { PromptRunCost } from "./PromptRunCost"; import { PromptRunTimer } from "./PromptRunTimer"; -let TableOutput; -try { - const mod = await import("../../../plugins/prompt-card/TableOutput"); - TableOutput = mod.TableOutput; -} catch { - // The component will remain null of it is not available -} -let ChallengeModal; -try { - const mod = await import("../../../plugins/challenge-modal/ChallengeModal"); - ChallengeModal = mod.ChallengeModal; -} catch { - // The component will remain null of it is not available -} +const TableOutput = await loadPlugin(() => + import("../../../plugins/prompt-card/TableOutput").then((m) => m.TableOutput), +); +const ChallengeModal = await loadPlugin(() => + import("../../../plugins/challenge-modal/ChallengeModal").then( + (m) => m.ChallengeModal, + ), +); -let LookupOutputTabs; -try { - const mod = await import( - "../../../plugins/lookup-studio/prompt-card/LookupOutputTabs" - ); - LookupOutputTabs = mod.LookupOutputTabs; -} catch { - // Cloud-only plugin — absent in OSS builds; LookupOutputTabs stays null. -} +// Cloud-only plugin — absent in OSS builds; LookupOutputTabs stays null. +const LookupOutputTabs = await loadPlugin(() => + import("../../../plugins/lookup-studio/prompt-card/LookupOutputTabs").then( + (m) => m.LookupOutputTabs, + ), +); -let getEnrichedCopyText; -try { - const mod = await import( - "../../../plugins/lookup-studio/prompt-card/getEnrichedCopyText" - ); - getEnrichedCopyText = mod.getEnrichedCopyText; -} catch { - // Cloud-only plugin — absent in OSS builds; falls back to raw copy text. -} +// Cloud-only plugin — absent in OSS builds; falls back to raw copy text. +const getEnrichedCopyText = await loadPlugin(() => + import("../../../plugins/lookup-studio/prompt-card/getEnrichedCopyText").then( + (m) => m.getEnrichedCopyText, + ), +); // Fallback to raw text — plugin throw on malformed enrichment shouldn't break Copy. const resolveCopyText = (promptOutputId, fallbackText) => { diff --git a/frontend/src/components/custom-tools/prompt-card/PromptOutputsModal.jsx b/frontend/src/components/custom-tools/prompt-card/PromptOutputsModal.jsx index ad579d125c..53539ee58f 100644 --- a/frontend/src/components/custom-tools/prompt-card/PromptOutputsModal.jsx +++ b/frontend/src/components/custom-tools/prompt-card/PromptOutputsModal.jsx @@ -2,6 +2,7 @@ import PropTypes from "prop-types"; import { Col, Row } from "@/components/ui/shims/antd-layout"; import { Modal } from "@/components/ui/shims/antd-overlays"; import { Typography } from "@/components/ui/shims/antd-typography"; +import { loadPlugin } from "../../../helpers/pluginLoader.js"; import usePromptOutput from "../../../hooks/usePromptOutput"; import { useCustomToolStore } from "../../../store/custom-tool-store"; import SpaceWrapper from "../../widgets/space-wrapper/SpaceWrapper"; @@ -9,13 +10,9 @@ import { TABLE } from "./constants"; import { DisplayPromptResult } from "./DisplayPromptResult"; import { ProfileIcon } from "./ProfileIcon"; -let TableOutput; -try { - const mod = await import("../../../plugins/prompt-card/TableOutput"); - TableOutput = mod.TableOutput; -} catch { - // The component will remain null of it is not available -} +const TableOutput = await loadPlugin(() => + import("../../../plugins/prompt-card/TableOutput").then((m) => m.TableOutput), +); function PromptOutputsModal({ open, diff --git a/frontend/src/components/custom-tools/settings-modal/SettingsModal.jsx b/frontend/src/components/custom-tools/settings-modal/SettingsModal.jsx index 99a68d5fb9..1f78615e9c 100644 --- a/frontend/src/components/custom-tools/settings-modal/SettingsModal.jsx +++ b/frontend/src/components/custom-tools/settings-modal/SettingsModal.jsx @@ -12,6 +12,7 @@ import { Modal } from "@/components/ui/shims/antd-overlays"; import { Menu } from "@/components/ui/shims/antd-structure"; import { Typography } from "@/components/ui/shims/antd-typography"; import { getMenuItem } from "../../../helpers/GetStaticData"; +import { loadPlugin } from "../../../helpers/pluginLoader.js"; import SpaceWrapper from "../../widgets/space-wrapper/SpaceWrapper"; import { CustomDataSettings } from "../custom-data-settings/CustomDataSettings"; import { CustomSynonyms } from "../custom-synonyms/CustomSynonyms"; @@ -20,26 +21,23 @@ import { PreAndPostAmbleModal } from "../pre-and-post-amble-modal/PreAndPostAmbl import "./SettingsModal.css"; -let SummarizeManager = null; +const SummarizeManager = await loadPlugin(() => + import("../../../plugins/summarize-manager/SummarizeManager").then( + (m) => m.SummarizeManager, + ), +); const EvaluationManager = null; -let ChallengeManager = null; -let HighlightManager = null; -try { - const smMod = await import( - "../../../plugins/summarize-manager/SummarizeManager" - ); - SummarizeManager = smMod.SummarizeManager; - const cmMod = await import( - "../../../plugins/challenge-manager/ChallengeManager" - ); - ChallengeManager = cmMod.ChallengeManager; - const hmMod = await import( - "../../../plugins/highlight-manager/HighlightManager" - ); - HighlightManager = hmMod.HighlightManager; -} catch { - // Component will remain null if it is not present. -} +const ChallengeManager = await loadPlugin(() => + import("../../../plugins/challenge-manager/ChallengeManager").then( + (m) => m.ChallengeManager, + ), +); +const HighlightManager = await loadPlugin(() => + import("../../../plugins/highlight-manager/HighlightManager").then( + (m) => m.HighlightManager, + ), +); + function SettingsModal({ open, setOpen, handleUpdateTool }) { const [selectedId, setSelectedId] = useState(1); const [menuItems, setMenuItems] = useState([]); diff --git a/frontend/src/components/custom-tools/tool-ide/ToolIde.jsx b/frontend/src/components/custom-tools/tool-ide/ToolIde.jsx index 86a6147ec4..17a6025ec1 100644 --- a/frontend/src/components/custom-tools/tool-ide/ToolIde.jsx +++ b/frontend/src/components/custom-tools/tool-ide/ToolIde.jsx @@ -1,5 +1,6 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { Col, Row } from "@/components/ui/shims/antd-layout"; +import { loadPlugin } from "../../../helpers/pluginLoader.js"; import { useAxiosPrivate } from "../../../hooks/useAxiosPrivate"; import { useExceptionHandler } from "../../../hooks/useExceptionHandler"; @@ -15,69 +16,58 @@ import "./ToolIde.css"; import usePostHogEvents from "../../../hooks/usePostHogEvents.js"; import { PageTitle } from "../../widgets/page-title/PageTitle.jsx"; -let PromptShareModal; -let PromptShareLink; -let CloneTitle; -let HeaderPublic; - -try { - const shareMod = await import( +const PromptShareModal = await loadPlugin(() => + import( "../../../plugins/prompt-studio-public-share/public-share-modal/PromptShareModal.jsx" - ); - PromptShareModal = shareMod.PromptShareModal; - const linkMod = await import( + ).then((m) => m.PromptShareModal), +); +const PromptShareLink = await loadPlugin(() => + import( "../../../plugins/prompt-studio-public-share/public-link-modal/PromptShareLink.jsx" - ); - PromptShareLink = linkMod.PromptShareLink; - const headerMod = await import( + ).then((m) => m.PromptShareLink), +); +const HeaderPublic = await loadPlugin(() => + import( "../../../plugins/prompt-studio-public-share/header-public/HeaderPublic.jsx" - ); - HeaderPublic = headerMod.HeaderPublic; -} catch { - // Do nothing if plugins are not loaded. -} -try { - const mod = await import( + ).then((m) => m.HeaderPublic), +); +const CloneTitle = await loadPlugin(() => + import( "../../../plugins/prompt-studio-clone/clone-title-modal/CloneTitle.jsx" - ); - CloneTitle = mod.CloneTitle; -} catch { - // Do nothing if plugins are not loaded. -} + ).then((m) => m.CloneTitle), +); // Cloud-only — OSS stub. -let useLookupDirtySeed = () => {}; -try { - const mod = await import( - "../../../plugins/lookup-studio/hooks/useLookupDirtySeed.js" - ); - useLookupDirtySeed = mod.useLookupDirtySeed; -} catch {} +const useLookupDirtySeed = await loadPlugin( + () => + import("../../../plugins/lookup-studio/hooks/useLookupDirtySeed.js").then( + (m) => m.useLookupDirtySeed, + ), + () => {}, +); // Cloud-only — OSS stub resolves true to skip the gate. -let useLookupExportGate = () => ({ - checkLookups: () => Promise.resolve(true), - modalEl: null, -}); -try { - const mod = await import( - "../../../plugins/lookup-studio/hooks/useLookupExportGate" - ); - useLookupExportGate = mod.useLookupExportGate; -} catch {} +const useLookupExportGate = await loadPlugin( + () => + import("../../../plugins/lookup-studio/hooks/useLookupExportGate").then( + (m) => m.useLookupExportGate, + ), + () => ({ + checkLookups: () => Promise.resolve(true), + modalEl: null, + }), +); /* * Cloud-only. Mounted here rather than beside either of its triggers: the * kebab menu unmounts its contents on click and the prompt card body unmounts * on collapse, so a drawer rendered in either was destroyed as it opened. */ -let LookupDrawerHost; -try { - const mod = await import( - "../../../plugins/lookup-studio/prompt-card/LookupDrawerHost" - ); - LookupDrawerHost = mod.LookupDrawerHost; -} catch {} +const LookupDrawerHost = await loadPlugin(() => + import("../../../plugins/lookup-studio/prompt-card/LookupDrawerHost").then( + (m) => m.LookupDrawerHost, + ), +); function ToolIde() { const [openSettings, setOpenSettings] = useState(false); diff --git a/frontend/src/components/custom-tools/tools-main/ToolsMainActionBtns.jsx b/frontend/src/components/custom-tools/tools-main/ToolsMainActionBtns.jsx index ba60b90c8f..e6c4a1c000 100644 --- a/frontend/src/components/custom-tools/tools-main/ToolsMainActionBtns.jsx +++ b/frontend/src/components/custom-tools/tools-main/ToolsMainActionBtns.jsx @@ -4,6 +4,7 @@ import { useNavigate, useParams } from "react-router-dom"; import { Button } from "@/components/ui/shims/antd-button"; import { Space } from "@/components/ui/shims/antd-layout"; import { Tooltip } from "@/components/ui/shims/antd-overlays"; +import { loadPlugin } from "../../../helpers/pluginLoader.js"; import { useAxiosPrivate } from "../../../hooks/useAxiosPrivate"; import { useExceptionHandler } from "../../../hooks/useExceptionHandler"; import usePostHogEvents from "../../../hooks/usePostHogEvents"; @@ -15,34 +16,24 @@ import { RunAllPrompts } from "../prompt-card/RunAllPrompts"; import { PromptsReorderModal } from "../prompts-reorder/PromptsReorderModal"; // Import single pass related components -let RunSinglePassBtn; -try { - const mod = await import( - "../../../plugins/run-single-pass-btn/RunSinglePassBtn" - ); - RunSinglePassBtn = mod.RunSinglePassBtn; -} catch { - // The variable will remain undefined if the component is not available -} +const RunSinglePassBtn = await loadPlugin(() => + import("../../../plugins/run-single-pass-btn/RunSinglePassBtn").then( + (m) => m.RunSinglePassBtn, + ), +); // Import simple prompt studio related components -let AddPromptBtn; -try { - const mod = await import( - "../../../plugins/simple-prompt-studio/AddPromptBtn" - ); - AddPromptBtn = mod.AddPromptBtn; -} catch { - // The variable will remain undefined if the component is not available -} +const AddPromptBtn = await loadPlugin(() => + import("../../../plugins/simple-prompt-studio/AddPromptBtn").then( + (m) => m.AddPromptBtn, + ), +); -let ChallengeModal; -try { - const mod = await import("../../../plugins/challenge-modal/ChallengeModal"); - ChallengeModal = mod.ChallengeModal; -} catch { - // The component will remain undefined if it is not available -} +const ChallengeModal = await loadPlugin(() => + import("../../../plugins/challenge-modal/ChallengeModal").then( + (m) => m.ChallengeModal, + ), +); function ToolsMainActionBtns() { const [openReorderModal, setOpenReorderModal] = useState(false); diff --git a/frontend/src/components/helpers/auth/PersistentLogin.jsx b/frontend/src/components/helpers/auth/PersistentLogin.jsx index ab0737ad46..fe695418cf 100644 --- a/frontend/src/components/helpers/auth/PersistentLogin.jsx +++ b/frontend/src/components/helpers/auth/PersistentLogin.jsx @@ -1,27 +1,23 @@ import { useEffect, useState } from "react"; import { Outlet, useLocation } from "react-router-dom"; +import { loadPlugin } from "../../../helpers/pluginLoader.js"; import useSessionValid from "../../../hooks/useSessionValid"; import { useSessionStore } from "../../../store/session-store"; import { PromptRun } from "../../custom-tools/prompt-card/PromptRun"; import { GenericLoader } from "../../generic-loader/GenericLoader"; import { SocketMessages } from "../socket-messages/SocketMessages"; -let selectedProductStore; +const selectedProductStore = await loadPlugin( + () => import("../../../plugins/store/select-product-store.js"), +); let selectedProduct; let setSelectedProduct; -let SELECTED_PRODUCT; -let PRODUCT_NAMES = {}; -try { - selectedProductStore = await import( - "../../../plugins/store/select-product-store.js" - ); - const commonMod = await import("../../../plugins/helpers/common"); - SELECTED_PRODUCT = commonMod.SELECTED_PRODUCT; - PRODUCT_NAMES = commonMod.PRODUCT_NAMES ?? {}; -} catch { - // Ignore if hook not available -} + +const { SELECTED_PRODUCT, PRODUCT_NAMES = {} } = await loadPlugin( + () => import("../../../plugins/helpers/common"), + {}, +); function PersistentLogin() { const [isLoading, setIsLoading] = useState(true); @@ -70,7 +66,8 @@ function PersistentLogin() { selectedProductQueryParam && Object.values(PRODUCT_NAMES).includes(selectedProductQueryParam) ) { - setSelectedProduct(selectedProductQueryParam); + // The store and the product constants load independently now. + setSelectedProduct?.(selectedProductQueryParam); } }, [selectedProductQueryParam]); diff --git a/frontend/src/components/helpers/auth/RequireAuth.jsx b/frontend/src/components/helpers/auth/RequireAuth.jsx index a856c2e03d..0e0e7004af 100644 --- a/frontend/src/components/helpers/auth/RequireAuth.jsx +++ b/frontend/src/components/helpers/auth/RequireAuth.jsx @@ -6,28 +6,20 @@ import { homePagePath, onboardCompleted, } from "../../../helpers/GetStaticData"; +import { loadPlugin } from "../../../helpers/pluginLoader.js"; import usePostHogEvents from "../../../hooks/usePostHogEvents"; import { useSessionStore } from "../../../store/session-store"; -let ProductFruitsManager; -try { - const mod = await import( - "../../../plugins/product-fruits/ProductFruitsManager" - ); - ProductFruitsManager = mod.ProductFruitsManager; -} catch { - // The component will remain null of it is not available -} -let selectedProductStore; +const ProductFruitsManager = await loadPlugin(() => + import("../../../plugins/product-fruits/ProductFruitsManager").then( + (m) => m.ProductFruitsManager, + ), +); +const selectedProductStore = await loadPlugin( + () => import("../../../plugins/store/select-product-store.js"), +); let isLlmWhisperer; let isVerticals; -try { - selectedProductStore = await import( - "../../../plugins/store/select-product-store.js" - ); -} catch { - // do nothing -} const RequireAuth = () => { const { sessionDetails } = useSessionStore(); diff --git a/frontend/src/components/helpers/auth/RequireGuest.jsx b/frontend/src/components/helpers/auth/RequireGuest.jsx index f0cec7ef7c..90f283b88a 100644 --- a/frontend/src/components/helpers/auth/RequireGuest.jsx +++ b/frontend/src/components/helpers/auth/RequireGuest.jsx @@ -5,18 +5,14 @@ import { onboardCompleted, publicRoutes, } from "../../../helpers/GetStaticData"; +import { loadPlugin } from "../../../helpers/pluginLoader.js"; import { useSessionStore } from "../../../store/session-store"; -let selectedProductStore; +const selectedProductStore = await loadPlugin( + () => import("../../../plugins/store/select-product-store.js"), +); let isLlmWhisperer; let isVerticals; -try { - selectedProductStore = await import( - "../../../plugins/store/select-product-store.js" - ); -} catch { - // do nothing -} const RequireGuest = () => { const { sessionDetails } = useSessionStore(); diff --git a/frontend/src/components/helpers/custom-tools/CustomToolsHelper.jsx b/frontend/src/components/helpers/custom-tools/CustomToolsHelper.jsx index 5a9dcb7574..386b2ab8ff 100644 --- a/frontend/src/components/helpers/custom-tools/CustomToolsHelper.jsx +++ b/frontend/src/components/helpers/custom-tools/CustomToolsHelper.jsx @@ -2,6 +2,7 @@ import { useEffect, useState } from "react"; import { Outlet, useNavigate, useParams } from "react-router-dom"; import { fetchAllPages } from "../../../helpers/pagination"; +import { loadPlugin } from "../../../helpers/pluginLoader.js"; import { useAxiosPrivate } from "../../../hooks/useAxiosPrivate"; import { useExceptionHandler } from "../../../hooks/useExceptionHandler"; import { useAlertStore } from "../../../store/alert-store"; @@ -11,27 +12,20 @@ import { useSocketCustomToolStore } from "../../../store/socket-custom-tool"; import { useTokenUsageStore } from "../../../store/token-usage-store"; import { SpinnerLoader } from "../../widgets/spinner-loader/SpinnerLoader"; -let shareManagerToolSource; -try { - const mod = await import( +// Without the plugin, the Not-found Page will be triggered. +const shareManagerToolSource = await loadPlugin(() => + import( "../../../plugins/prompt-studio-public-share/helpers/PublicShareAPIs" + ).then((m) => m.shareManagerToolSource), +); + +const { fetchLookupAssignments, fetchLookupOutputs, resetLookupAssignments } = + await loadPlugin( + () => + import("../../../plugins/lookup-studio/store/useFetchLookupAssignments"), + {}, ); - shareManagerToolSource = mod.shareManagerToolSource; -} catch { - // Do nothing, Not-found Page will be triggered. -} -let fetchLookupAssignments; -let fetchLookupOutputs; -let resetLookupAssignments; -try { - const mod = await import( - "../../../plugins/lookup-studio/store/useFetchLookupAssignments" - ); - fetchLookupAssignments = mod.fetchLookupAssignments; - fetchLookupOutputs = mod.fetchLookupOutputs; - resetLookupAssignments = mod.resetLookupAssignments; -} catch {} function CustomToolsHelper() { const [isLoading, setIsLoading] = useState(true); const { id } = useParams(); diff --git a/frontend/src/components/input-output/add-source/AddSource.jsx b/frontend/src/components/input-output/add-source/AddSource.jsx index 60b1585f81..6bd29e943f 100644 --- a/frontend/src/components/input-output/add-source/AddSource.jsx +++ b/frontend/src/components/input-output/add-source/AddSource.jsx @@ -4,6 +4,7 @@ import PropTypes from "prop-types"; import { useEffect, useMemo, useState } from "react"; import { Typography } from "@/components/ui/shims/antd-typography"; +import { loadPlugin } from "../../../helpers/pluginLoader.js"; import { useAxiosPrivate } from "../../../hooks/useAxiosPrivate"; import { useExceptionHandler } from "../../../hooks/useExceptionHandler"; import useRequestUrl from "../../../hooks/useRequestUrl"; @@ -11,30 +12,27 @@ import { useAlertStore } from "../../../store/alert-store"; import { EmptyState } from "../../widgets/empty-state/EmptyState"; import { ConfigureDs } from "../configure-ds/ConfigureDs"; -let transformLlmWhispererJsonSchema; -let LLMW_V2_ID; -let PLAN_TYPES; -let unstractSubscriptionPlanStore; -let llmWhipererAdapterSchema; -try { - const schemaMod = await import( - "../../../plugins/unstract-subscription/helper/transformLlmWhispererJsonSchema" - ); - transformLlmWhispererJsonSchema = schemaMod.transformLlmWhispererJsonSchema; - LLMW_V2_ID = schemaMod.LLMW_V2_ID; - const constantsMod = await import( - "../../../plugins/unstract-subscription/helper/constants" - ); - PLAN_TYPES = constantsMod.PLAN_TYPES; - unstractSubscriptionPlanStore = await import( - "../../../plugins/store/unstract-subscription-plan-store" - ); - llmWhipererAdapterSchema = await import( - "../../../plugins/unstract-subscription/hooks/useLlmWhispererAdapterSchema.js" - ); -} catch { - // Ignore if not available -} +const { transformLlmWhispererJsonSchema, LLMW_V2_ID } = await loadPlugin( + () => + import( + "../../../plugins/unstract-subscription/helper/transformLlmWhispererJsonSchema" + ), + {}, +); +const PLAN_TYPES = await loadPlugin(() => + import("../../../plugins/unstract-subscription/helper/constants").then( + (m) => m.PLAN_TYPES, + ), +); +const unstractSubscriptionPlanStore = await loadPlugin( + () => import("../../../plugins/store/unstract-subscription-plan-store"), +); +const llmWhipererAdapterSchema = await loadPlugin( + () => + import( + "../../../plugins/unstract-subscription/hooks/useLlmWhispererAdapterSchema.js" + ), +); function AddSource({ selectedSourceId, diff --git a/frontend/src/components/log-in/Login.jsx b/frontend/src/components/log-in/Login.jsx index 54b55bb83d..7c22111440 100644 --- a/frontend/src/components/log-in/Login.jsx +++ b/frontend/src/components/log-in/Login.jsx @@ -2,17 +2,14 @@ import { Button } from "@/components/ui/shims/antd-button"; import { Col, Row } from "@/components/ui/shims/antd-layout"; import { getBaseUrl } from "../../helpers/GetStaticData"; +import { loadPlugin } from "../../helpers/pluginLoader.js"; import "./Login.css"; import { UnstractBlackLogo } from "../../assets"; import { ProductContentLayout } from "./ProductContentLayout"; -let LoginForm = null; -try { - const mod = await import("../../plugins/login-form/LoginForm"); - LoginForm = mod.LoginForm; -} catch { - // Plugin not available (OSS version) -} +const LoginForm = await loadPlugin(() => + import("../../plugins/login-form/LoginForm").then((m) => m.LoginForm), +); function Login() { const baseUrl = getBaseUrl(); diff --git a/frontend/src/components/metrics-dashboard/MetricsDashboard.jsx b/frontend/src/components/metrics-dashboard/MetricsDashboard.jsx index 3e814bf027..2dfda2a613 100644 --- a/frontend/src/components/metrics-dashboard/MetricsDashboard.jsx +++ b/frontend/src/components/metrics-dashboard/MetricsDashboard.jsx @@ -19,6 +19,7 @@ import { Typography } from "@/components/ui/shims/antd-typography"; import { EmptyPlaceholder } from "../../assets"; import { evictExpiredCache } from "../../helpers/metricsCache"; +import { loadPlugin } from "../../helpers/pluginLoader.js"; import { useMetricsOverview, useRecentActivity, @@ -33,27 +34,19 @@ import { RecentActivity } from "./RecentActivity"; import "./MetricsDashboard.css"; -// Cloud-only: Plan banner with subscription details -let PlanBanner; -try { - const mod = await import( - "../../plugins/unstract-subscription/components/PlanBanner.jsx" - ); - PlanBanner = mod.PlanBanner; -} catch { - // Plugin unavailable - no banner on OSS -} +// Cloud-only: Plan banner with subscription details (no banner on OSS) +const PlanBanner = await loadPlugin(() => + import("../../plugins/unstract-subscription/components/PlanBanner.jsx").then( + (m) => m.PlanBanner, + ), +); -// Cloud-only: Subscription usage tab -let SubscriptionUsageTab; -try { - const mod = await import( +// Cloud-only: Subscription usage tab (no subscription tab on OSS) +const SubscriptionUsageTab = await loadPlugin(() => + import( "../../plugins/unstract-subscription/components/SubscriptionUsageTab.jsx" - ); - SubscriptionUsageTab = mod.SubscriptionUsageTab; -} catch { - // Plugin unavailable - no subscription tab on OSS -} + ).then((m) => m.SubscriptionUsageTab), +); const { RangePicker } = DatePicker; diff --git a/frontend/src/components/navigations/side-nav-bar/SideNavBar.jsx b/frontend/src/components/navigations/side-nav-bar/SideNavBar.jsx index cfa789c94d..f5dc4915a9 100644 --- a/frontend/src/components/navigations/side-nav-bar/SideNavBar.jsx +++ b/frontend/src/components/navigations/side-nav-bar/SideNavBar.jsx @@ -25,6 +25,7 @@ import { getLocalStorageValue, setLocalStorageValue, } from "../../../helpers/localStorage"; +import { loadPlugin } from "../../../helpers/pluginLoader.js"; import { useSessionStore } from "../../../store/session-store"; import "./SideNavBar.css"; @@ -32,67 +33,40 @@ import "../../settings/settings/Settings.css"; const { Sider } = Layout; -let getMenuItem; -try { - getMenuItem = await import("../../../plugins/app-deployment/getMenuItem"); -} catch { - // Plugin unavailable. -} +const getMenuItem = await loadPlugin( + () => import("../../../plugins/app-deployment/getMenuItem"), +); -let sideMenu; -try { - sideMenu = await import("../../../plugins/hooks/useSideMenu"); -} catch { - // Plugin unavailable. -} +const sideMenu = await loadPlugin( + () => import("../../../plugins/hooks/useSideMenu"), +); let unstractSubscriptionPlan; -let unstractSubscriptionPlanStore; -let UNSTRACT_SUBSCRIPTION_PLANS; -try { - unstractSubscriptionPlanStore = await import( - "../../../plugins/store/unstract-subscription-plan-store" - ); - const unstractSubscriptionConstants = await import( - "../../../plugins/unstract-subscription/helper/constants" - ); - UNSTRACT_SUBSCRIPTION_PLANS = - unstractSubscriptionConstants?.UNSTRACT_SUBSCRIPTION_PLANS; -} catch { - // Plugin unavailable. -} - -let selectedProductStore; +const unstractSubscriptionPlanStore = await loadPlugin( + () => import("../../../plugins/store/unstract-subscription-plan-store"), +); +const UNSTRACT_SUBSCRIPTION_PLANS = await loadPlugin(() => + import("../../../plugins/unstract-subscription/helper/constants").then( + (m) => m.UNSTRACT_SUBSCRIPTION_PLANS, + ), +); + +const selectedProductStore = await loadPlugin( + () => import("../../../plugins/store/select-product-store.js"), +); let selectedProduct; -try { - selectedProductStore = await import( - "../../../plugins/store/select-product-store.js" - ); -} catch { - // Ignore if hook not available -} - -let agenticPromptStudioEnabled = false; -try { - await import("../../../plugins/agentic-prompt-studio"); - agenticPromptStudioEnabled = true; -} catch { - // Plugin unavailable -} - -let lookupStudioEnabled = false; -try { - await import("../../../plugins/lookup-studio"); - lookupStudioEnabled = true; -} catch {} - -let manualReviewSettingsEnabled = false; -try { - await import("../../../plugins/manual-review/settings/Settings.jsx"); - manualReviewSettingsEnabled = true; -} catch { - // Plugin unavailable -} + +const agenticPromptStudioEnabled = + (await loadPlugin(() => import("../../../plugins/agentic-prompt-studio"))) !== + null; + +const lookupStudioEnabled = + (await loadPlugin(() => import("../../../plugins/lookup-studio"))) !== null; + +const manualReviewSettingsEnabled = + (await loadPlugin( + () => import("../../../plugins/manual-review/settings/Settings.jsx"), + )) !== null; const getSettingsMenuItems = (orgName, isAdmin) => [ { diff --git a/frontend/src/components/navigations/top-nav-bar/TopNavBar.jsx b/frontend/src/components/navigations/top-nav-bar/TopNavBar.jsx index 98fdbdd635..469c674dab 100644 --- a/frontend/src/components/navigations/top-nav-bar/TopNavBar.jsx +++ b/frontend/src/components/navigations/top-nav-bar/TopNavBar.jsx @@ -23,6 +23,7 @@ import { onboardCompleted, THEME, } from "../../../helpers/GetStaticData.js"; +import { loadPlugin } from "../../../helpers/pluginLoader.js"; import useLogout from "../../../hooks/useLogout.js"; import "../../../layouts/page-layout/PageLayout.css"; import { useSessionStore } from "../../../store/session-store.js"; @@ -32,46 +33,27 @@ import { useExceptionHandler } from "../../../hooks/useExceptionHandler.jsx"; import { useAlertStore } from "../../../store/alert-store.js"; import { ConfirmModal } from "../../widgets/confirm-modal/ConfirmModal.jsx"; -let TrialDaysInfo; -try { - const mod = await import( +const TrialDaysInfo = await loadPlugin(() => + import( "../../../plugins/unstract-subscription/components/TrialDaysInfo.jsx" - ); - TrialDaysInfo = mod.default; -} catch { - // Plugin not found -} + ).then((m) => m.default), +); -let selectedProductStore; +const selectedProductStore = await loadPlugin( + () => import("../../../plugins/store/select-product-store.js"), +); let selectedProduct; -try { - selectedProductStore = await import( - "../../../plugins/store/select-product-store.js" - ); -} catch { - // Ignore if hook not available -} +const PlatformDropdown = await loadPlugin(() => + import("../../../plugins/platform-dropdown/PlatformDropDown.jsx").then( + (m) => m.PlatformDropdown, + ), +); -let PlatformDropdown; -try { - const mod = await import( - "../../../plugins/platform-dropdown/PlatformDropDown.jsx" - ); - PlatformDropdown = mod.PlatformDropdown; -} catch { - // Plugin not found -} - -let WhispererLogo; -let WhispererDarkLogo; -try { - const mod = await import("../../../plugins/assets/llmWhisperer/index.js"); - WhispererLogo = mod.WhispererLogo; - WhispererDarkLogo = mod.LlmWhispererLogo; -} catch { - // Ignore if hook not available -} +const { WhispererLogo, LlmWhispererLogo: WhispererDarkLogo } = await loadPlugin( + () => import("../../../plugins/assets/llmWhisperer/index.js"), + {}, +); /* * `Logo` is injected rather than hardcoded: the top bar is a light surface in @@ -110,35 +92,25 @@ const CustomLogo = ({ onClick, className, Logo }) => { // `APIHubLogo`/`WhispererLogo` are the WHITE-ink marks (named for the dark bar // they were drawn for); `APIHubDarkLogo`/`LlmWhispererLogo` are the dark-ink // ones. Both are pulled so the bar can pick by theme. -let APIHubLogo; -let APIHubDarkLogo; -try { - const mod = await import("../../../plugins/assets/verticals/index.js"); - APIHubLogo = mod.APIHubLogo; - APIHubDarkLogo = mod.APIHubDarkLogo; -} catch { - // Ignore if hook not available -} +const { APIHubLogo, APIHubDarkLogo } = await loadPlugin( + () => import("../../../plugins/assets/verticals/index.js"), + {}, +); let unstractSubscriptionPlan; -let unstractSubscriptionPlanStore; -let UNSTRACT_SUBSCRIPTION_PLANS; -let UnstractPricingMenuLink; -try { - unstractSubscriptionPlanStore = await import( - "../../../plugins/store/unstract-subscription-plan-store" - ); - const constantsMod = await import( - "../../../plugins/unstract-subscription/helper/constants" - ); - UNSTRACT_SUBSCRIPTION_PLANS = constantsMod.UNSTRACT_SUBSCRIPTION_PLANS; - const menuMod = await import( +const unstractSubscriptionPlanStore = await loadPlugin( + () => import("../../../plugins/store/unstract-subscription-plan-store"), +); +const UNSTRACT_SUBSCRIPTION_PLANS = await loadPlugin(() => + import("../../../plugins/unstract-subscription/helper/constants").then( + (m) => m.UNSTRACT_SUBSCRIPTION_PLANS, + ), +); +const UnstractPricingMenuLink = await loadPlugin(() => + import( "../../../plugins/unstract-subscription/components/UnstractPricingMenuLink.jsx" - ); - UnstractPricingMenuLink = menuMod.UnstractPricingMenuLink; -} catch { - // Plugin unavailable. -} + ).then((m) => m.UnstractPricingMenuLink), +); function TopNavBar({ isSimpleLayout, topNavBarOptions }) { const navigate = useNavigate(); diff --git a/frontend/src/components/settings/platform/PlatformSettings.jsx b/frontend/src/components/settings/platform/PlatformSettings.jsx index 51131c115f..c8eab55335 100644 --- a/frontend/src/components/settings/platform/PlatformSettings.jsx +++ b/frontend/src/components/settings/platform/PlatformSettings.jsx @@ -8,6 +8,7 @@ import { Col, Row } from "@/components/ui/shims/antd-layout"; import { Divider, Tag } from "@/components/ui/shims/antd-leaves"; import { Typography } from "@/components/ui/shims/antd-typography"; +import { loadPlugin } from "../../../helpers/pluginLoader.js"; import { useAxiosPrivate } from "../../../hooks/useAxiosPrivate"; import { IslandLayout } from "../../../layouts/island-layout/IslandLayout.jsx"; import { useAlertStore } from "../../../store/alert-store"; @@ -24,13 +25,10 @@ import { SettingsLayout } from "../settings-layout/SettingsLayout.jsx"; // there. Detect the enterprise build by probing for an enterprise-only plugin // (absent in OSS) and hide the control entirely otherwise. Mirrors the // plugin-gating idiom used in SideNavBar. -let isEnterpriseBuild = false; -try { - await import("../../../plugins/store/unstract-subscription-plan-store"); - isEnterpriseBuild = true; -} catch { - // OSS build — enterprise plugins are not bundled. -} +const isEnterpriseBuild = + (await loadPlugin( + () => import("../../../plugins/store/unstract-subscription-plan-store"), + )) !== null; const defaultKeys = [ { diff --git a/frontend/src/helpers/GetStaticData.js b/frontend/src/helpers/GetStaticData.js index 4a0b5b28dc..bbb8a859d0 100644 --- a/frontend/src/helpers/GetStaticData.js +++ b/frontend/src/helpers/GetStaticData.js @@ -3,13 +3,13 @@ import moment from "moment"; import momentTz from "moment-timezone"; import { v4 as uuidv4 } from "uuid"; -let cloudHomePagePath; -try { - const mod = await import("../plugins/unstract-subscription/helper/constants"); - cloudHomePagePath = mod.cloudHomePagePath; -} catch { - // Ignore if plugin not available -} +import { loadPlugin } from "./pluginLoader.js"; + +const cloudHomePagePath = await loadPlugin(() => + import("../plugins/unstract-subscription/helper/constants").then( + (m) => m.cloudHomePagePath, + ), +); const THEME = { DARK: "dark", diff --git a/frontend/src/helpers/pluginLoader.js b/frontend/src/helpers/pluginLoader.js index eefb192266..ae040f71b0 100644 --- a/frontend/src/helpers/pluginLoader.js +++ b/frontend/src/helpers/pluginLoader.js @@ -1,33 +1,60 @@ -// Classify a dynamic plugin-import failure as "plugin not shipped" -// (expected in OSS builds) vs. a real load failure. +// The one way to load an optional enterprise plugin that may be absent (OSS +// builds ship without `src/plugins/`). Route elements use `lazyPlugin` +// (pluginRegistry.js); everything else — components, hooks, stores, helpers, +// constants — goes through `loadPlugin`. // -// The PRIMARY signal in the shipped Vite bundle: vite.config.js's -// `optionalPluginImports` plugin resolves a missing optional-plugin path -// to a stub module whose body is `throw new Error('Optional plugin not -// available')`. That is the actual error an absent plugin throws in -// production, so it must be matched here or every OSS page load misfires -// the consumer's error branch. +// Zero-dependency on purpose: consumers include modules that import each +// other (Router/useMainAppRoutes/PageLayout), and this helper must never +// re-introduce an import cycle. + +// The only error that means "this plugin was not shipped" is the build-time +// stub vite.config.js's `optionalPluginImports` resolves a missing optional +// plugin to: `throw new Error('Optional plugin not available')`. +// `MODULE_NOT_FOUND` / "Cannot find module" are the Node equivalents, kept for +// anything that loads these modules outside Vite. // -// LIMITATION: this still isn't fully sound — a present-but-failed-to-load -// chunk (transient CDN/origin 5xx, a stale hashed asset) surfaces as -// "Failed to fetch dynamically imported module", and `MODULE_NOT_FOUND` -// is a Node/CJS code that only appears under jsdom/vitest, not the -// shipped bundle. So a transient chunk-load failure can be misread as -// "missing" and silently disable whatever the consumer gates on it — -// a route in the routers, the status banner in PageLayout — for that -// session. Same outcome as a bare `catch`, but genuine errors in the -// present-plugin case at least get logged. Centralized so any future -// hardening lands once. -// -// Zero-dependency on purpose: consumers include modules that import -// each other (Router/useMainAppRoutes/PageLayout), and this helper -// must never re-introduce an import cycle. -export function isModuleMissing(err) { +// "Failed to fetch dynamically imported module" is deliberately NOT here: it +// is a plugin that IS shipped but whose chunk failed to load (CDN/origin +// blip, stale hashed asset, or a syntax error the dev server refused to +// serve). That is a real failure and must be logged, not mistaken for absence. +export function isPluginAbsent(err) { const msg = err?.message || ""; return ( - err?.code === "MODULE_NOT_FOUND" || msg.includes("Optional plugin not available") || - msg.includes("Failed to fetch dynamically imported module") || + err?.code === "MODULE_NOT_FOUND" || msg.includes("Cannot find module") ); } + +// Load an optional plugin, resolving to `fallback` when it is unavailable. +// +// const TrialDaysInfo = await loadPlugin(() => +// import("../plugins/x/TrialDaysInfo.jsx").then((m) => m.default), +// ); +// +// - `importer` is a thunk so the literal `../plugins/...` path stays at the +// call site, where Vite can statically resolve it (and stub it in OSS). +// Have it return exactly the value you need — pick the export in `.then`. +// - Resolves to `fallback` when the plugin is absent, when the import fails +// for any other reason, or when the importer yields `undefined`/`null` +// (e.g. the export was renamed, or vitest's empty stub module). +// - Absence is the expected OSS case and stays silent. Every other failure is +// logged here, so a broken plugin is never indistinguishable from a +// missing one. +// +// Load ONE plugin module per call: sharing a call (or a try block) between +// plugins lets one missing plugin disable the others. +export async function loadPlugin(importer, fallback = null) { + try { + return (await importer()) ?? fallback; + } catch (err) { + if (!isPluginAbsent(err)) { + reportPluginLoadError(err); + } + return fallback; + } +} + +function reportPluginLoadError(err) { + console.error("[plugin] failed to load; using the fallback instead", err); +} diff --git a/frontend/src/helpers/pluginLoader.test.js b/frontend/src/helpers/pluginLoader.test.js new file mode 100644 index 0000000000..90b01af654 --- /dev/null +++ b/frontend/src/helpers/pluginLoader.test.js @@ -0,0 +1,88 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { isPluginAbsent, loadPlugin } from "./pluginLoader.js"; + +const absent = () => Promise.reject(new Error("Optional plugin not available")); + +describe("loadPlugin", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("resolves to what the importer returns", async () => { + const Comp = () => null; + await expect(loadPlugin(() => Promise.resolve(Comp))).resolves.toBe(Comp); + }); + + it("keeps falsy values that are not null/undefined", async () => { + await expect(loadPlugin(() => Promise.resolve(false), true)).resolves.toBe( + false, + ); + }); + + it("falls back silently when the plugin is absent", async () => { + const error = vi.spyOn(console, "error").mockImplementation(() => {}); + const fallback = () => null; + + await expect(loadPlugin(absent, fallback)).resolves.toBe(fallback); + expect(error).not.toHaveBeenCalled(); + }); + + it("defaults the fallback to null", async () => { + await expect(loadPlugin(absent)).resolves.toBeNull(); + }); + + it("falls back when the picked export is missing", async () => { + await expect( + loadPlugin(() => Promise.resolve({}).then((m) => m.Missing), "fb"), + ).resolves.toBe("fb"); + }); + + it("logs a broken plugin and still falls back", async () => { + const error = vi.spyOn(console, "error").mockImplementation(() => {}); + const boom = new Error("boom at module evaluation"); + + await expect(loadPlugin(() => Promise.reject(boom), "fb")).resolves.toBe( + "fb", + ); + expect(error).toHaveBeenCalledWith( + expect.stringContaining("[plugin]"), + boom, + ); + }); + + it("logs a chunk that failed to fetch instead of treating it as absent", async () => { + const error = vi.spyOn(console, "error").mockImplementation(() => {}); + const chunk = new TypeError( + "Failed to fetch dynamically imported module: /assets/x-abc.js", + ); + + await expect(loadPlugin(() => Promise.reject(chunk))).resolves.toBeNull(); + expect(error).toHaveBeenCalledOnce(); + }); + + it("catches an importer that throws synchronously", async () => { + vi.spyOn(console, "error").mockImplementation(() => {}); + await expect( + loadPlugin(() => { + throw new Error("sync"); + }, "fb"), + ).resolves.toBe("fb"); + }); +}); + +describe("isPluginAbsent", () => { + it.each([ + [new Error("Optional plugin not available"), true], + [Object.assign(new Error("x"), { code: "MODULE_NOT_FOUND" }), true], + [new Error("Cannot find module '../plugins/x'"), true], + [ + new TypeError("Failed to fetch dynamically imported module: /a.js"), + false, + ], + [new Error("boom"), false], + [undefined, false], + ])("classifies %s as absent=%s", (err, expected) => { + expect(isPluginAbsent(err)).toBe(expected); + }); +}); diff --git a/frontend/src/helpers/pluginRegistry.js b/frontend/src/helpers/pluginRegistry.js index 5191dfc53b..7a412873fe 100644 --- a/frontend/src/helpers/pluginRegistry.js +++ b/frontend/src/helpers/pluginRegistry.js @@ -1,20 +1,7 @@ import { lazy } from "react"; import { NotFound } from "../components/error/NotFound/NotFound.jsx"; - -// The only error that means "this plugin was not shipped" is the build-time -// stub vite.config.js's `optionalPluginImports` resolves a missing optional -// plugin to: `throw new Error('Optional plugin not available')`. We match that -// exact signal and nothing else. -// -// We deliberately do NOT reuse the broader `isModuleMissing` here: it also -// matches "Failed to fetch dynamically imported module", which is a TRANSIENT -// chunk-load failure of a plugin that IS shipped (CDN/origin blip, stale hashed -// asset). Treating that as "absent" would silently render NotFound for a real, -// momentarily-unreachable route instead of surfacing the failure. -function isPluginAbsent(err) { - return (err?.message || "").includes("Optional plugin not available"); -} +import { isPluginAbsent } from "./pluginLoader.js"; // Wrap an enterprise plugin's dynamic import as a lazy route element. The // plugin chunk is fetched only when the element actually renders (i.e. on diff --git a/frontend/src/hooks/usePromptOutput.js b/frontend/src/hooks/usePromptOutput.js index 6f4ef40e4d..ebd95ed31c 100644 --- a/frontend/src/hooks/usePromptOutput.js +++ b/frontend/src/hooks/usePromptOutput.js @@ -1,47 +1,37 @@ import { useParams } from "react-router-dom"; +import { loadPlugin } from "../helpers/pluginLoader.js"; import { useCustomToolStore } from "../store/custom-tool-store"; import { usePromptOutputStore } from "../store/prompt-output-store"; import { useSessionStore } from "../store/session-store"; import { useTokenUsageStore } from "../store/token-usage-store"; import { useAxiosPrivate } from "./useAxiosPrivate"; -let promptOutputApiSps; -try { - const mod = await import("../plugins/simple-prompt-studio/helper"); - promptOutputApiSps = mod.promptOutputApiSps; -} catch { - // The component will remain null of it is not available -} -let publicOutputsApi; -try { - const mod = await import( - "../plugins/prompt-studio-public-share/helpers/PublicShareAPIs" - ); - publicOutputsApi = mod.publicOutputsApi; -} catch { - // The component will remain null of it is not available -} - -let handleLookupOutput; -try { - const mod = await import( - "../plugins/lookup-studio/prompt-card/handleLookupOutput" - ); - handleLookupOutput = mod.handleLookupOutput; -} catch (error) { - // Surface chunk-load failures — silent catch hid them. - console.warn("[usePromptOutput] handleLookupOutput unavailable:", error); -} +const promptOutputApiSps = await loadPlugin(() => + import("../plugins/simple-prompt-studio/helper").then( + (m) => m.promptOutputApiSps, + ), +); +const publicOutputsApi = await loadPlugin(() => + import("../plugins/prompt-studio-public-share/helpers/PublicShareAPIs").then( + (m) => m.publicOutputsApi, + ), +); + +const handleLookupOutput = await loadPlugin(() => + import("../plugins/lookup-studio/prompt-card/handleLookupOutput").then( + (m) => m.handleLookupOutput, + ), +); // Cloud-only extractor; OSS no-op. Signature matches plugin helper. -let getEnrichmentFromItem = (_item) => null; -try { - const mod = await import("../plugins/lookup-enriched-toggle/helpers"); - getEnrichmentFromItem = mod.getEnrichmentFromItem; -} catch (error) { - console.warn("[usePromptOutput] getEnrichmentFromItem unavailable:", error); -} +const getEnrichmentFromItem = await loadPlugin( + () => + import("../plugins/lookup-enriched-toggle/helpers").then( + (m) => m.getEnrichmentFromItem, + ), + (_item) => null, +); const usePromptOutput = () => { const { sessionDetails } = useSessionStore(); diff --git a/frontend/src/hooks/useSessionValid.js b/frontend/src/hooks/useSessionValid.js index a2c2c0e092..d96636d4e7 100644 --- a/frontend/src/hooks/useSessionValid.js +++ b/frontend/src/hooks/useSessionValid.js @@ -3,33 +3,27 @@ import Cookies from "js-cookie"; import { useNavigate } from "react-router-dom"; import { listFlags } from "../helpers/FeatureFlagsData.js"; import { getSessionData } from "../helpers/GetSessionData"; +import { loadPlugin } from "../helpers/pluginLoader.js"; import { useExceptionHandler } from "../hooks/useExceptionHandler.jsx"; import { useAlertStore } from "../store/alert-store"; import { useSessionStore } from "../store/session-store"; import { useUserSession } from "./useUserSession.js"; -let isPlatformAdmin; -try { - const mod = await import("../plugins/hooks/usePlatformAdmin.js"); - isPlatformAdmin = mod.usePlatformAdmin(); -} catch { - // Plugin not available -} +const isPlatformAdmin = await loadPlugin(() => + import("../plugins/hooks/usePlatformAdmin.js").then((m) => + m.usePlatformAdmin(), + ), +); let selectedProduct; -let selectedProductStore; -let PRODUCT_NAMES = {}; - -try { - selectedProductStore = await import( - "../plugins/store/select-product-store.js" - ); - const helperMod = await import("../plugins/llm-whisperer/helper"); - // Guard against empty/missing module export - PRODUCT_NAMES = helperMod.PRODUCT_NAMES ?? {}; -} catch { - // Ignore if hook not available -} +const selectedProductStore = await loadPlugin( + () => import("../plugins/store/select-product-store.js"), +); +const { PRODUCT_NAMES = {} } = await loadPlugin( + () => import("../plugins/llm-whisperer/helper"), + {}, +); + function useSessionValid() { const setSessionDetails = useSessionStore((state) => state.setSessionDetails); const handleException = useExceptionHandler(); diff --git a/frontend/src/layouts/page-layout/PageLayout.jsx b/frontend/src/layouts/page-layout/PageLayout.jsx index 7a5c6a483e..52ae250644 100644 --- a/frontend/src/layouts/page-layout/PageLayout.jsx +++ b/frontend/src/layouts/page-layout/PageLayout.jsx @@ -11,25 +11,14 @@ import { getLocalStorageValue, setLocalStorageValue, } from "../../helpers/localStorage"; -import { isModuleMissing } from "../../helpers/pluginLoader.js"; +import { loadPlugin } from "../../helpers/pluginLoader.js"; // Optional status banner contributed by the marketplace plugin, when -// present. The plugin is absent in OSS builds — the import fails and -// nothing is mounted. The banner self-manages its visibility. -let MarketplacePendingBanner; -try { - const marketplaceMod = await import("../../plugins/marketplace"); - MarketplacePendingBanner = marketplaceMod.MarketplacePendingBanner; -} catch (err) { - // Missing plugin is the expected case; surface anything else so a - // broken plugin doesn't silently unmount its UI. - if (!isModuleMissing(err)) { - console.error( - "[marketplace] MarketplacePendingBanner import failed unexpectedly", - err, - ); - } -} +// present. The plugin is absent in OSS builds, so nothing is mounted. The +// banner self-manages its visibility. +const MarketplacePendingBanner = await loadPlugin(() => + import("../../plugins/marketplace").then((m) => m.MarketplacePendingBanner), +); function PageLayout({ sideBarOptions, diff --git a/frontend/src/pages/CustomTools.jsx b/frontend/src/pages/CustomTools.jsx index 0864d9d262..9277586d94 100644 --- a/frontend/src/pages/CustomTools.jsx +++ b/frontend/src/pages/CustomTools.jsx @@ -2,6 +2,7 @@ import { useEffect, useState } from "react"; import { useLocation } from "react-router-dom"; import { ListOfTools } from "../components/custom-tools/list-of-tools/ListOfTools"; +import { loadPlugin } from "../helpers/pluginLoader.js"; const TAB_OPTIONS = ["Projects", "Look-Ups"]; @@ -13,9 +14,13 @@ function CustomTools() { ); useEffect(() => { - import("../plugins/lookup-studio") - .then((mod) => setLookupListComp(() => mod.LookupList)) - .catch(() => {}); + loadPlugin(() => + import("../plugins/lookup-studio").then((m) => m.LookupList), + ).then((LookupList) => { + if (LookupList) { + setLookupListComp(() => LookupList); + } + }); }, []); useEffect(() => { diff --git a/frontend/src/pages/UnstractAdministrationPage.jsx b/frontend/src/pages/UnstractAdministrationPage.jsx index c51486c2b7..634d3fd014 100644 --- a/frontend/src/pages/UnstractAdministrationPage.jsx +++ b/frontend/src/pages/UnstractAdministrationPage.jsx @@ -1,17 +1,13 @@ import "./UnstractAdministrationPage.css"; +import { loadPlugin } from "../helpers/pluginLoader.js"; import { useSessionStore } from "../store/session-store"; -let UnstractAdministration; - -try { - const mod = await import( +// Cloud-only feature, not available in OSS +const UnstractAdministration = await loadPlugin(() => + import( "../plugins/subscription-admin/components/UnstractAdministration.jsx" - ); - UnstractAdministration = mod.UnstractAdministration; -} catch { - // NOSONAR - // Cloud-only feature, not available in OSS -} + ).then((m) => m.UnstractAdministration), +); function UnstractAdministrationPage() { const { sessionDetails } = useSessionStore(); diff --git a/frontend/src/routes/Router.jsx b/frontend/src/routes/Router.jsx index 05bcea6b18..45e099f013 100644 --- a/frontend/src/routes/Router.jsx +++ b/frontend/src/routes/Router.jsx @@ -14,7 +14,7 @@ import { RequireGuest } from "../components/helpers/auth/RequireGuest.jsx"; import { OAuthStatus } from "../components/oauth-ds/oauth-status/OAuthStatus.jsx"; import { ErrorBoundary } from "../components/widgets/error-boundary/ErrorBoundary.jsx"; import { lazyNamed } from "../helpers/lazyNamed.js"; -import { isModuleMissing } from "../helpers/pluginLoader.js"; +import { loadPlugin } from "../helpers/pluginLoader.js"; import { lazyPlugin } from "../helpers/pluginRegistry.js"; import { LandingPage } from "../pages/LandingPage.jsx"; import { useMainAppRoutes } from "./useMainAppRoutes.jsx"; @@ -120,30 +120,22 @@ const LlmWhispererCustomCheckoutPage = lazyPlugin( // These plugins export hooks that RETURN a tree consumed // synchronously during render, so they cannot be wrapped in React.lazy and -// are loaded with a guarded await. OSS resolves these to the stub (caught -// below); cloud loads them. NOTE: in cloud these two modules still load on +// are loaded with loadPlugin at module evaluation. OSS resolves these to the +// stub; cloud loads them. NOTE: in cloud these two modules still load on // /landing because the await runs at module evaluation. Fully deferring them // requires the plugins themselves (in unstract-cloud) to lazy-load their own // page imports — tracked as a follow-up. -let llmWhispererRouter; -try { - const mod = await import("../plugins/routes/useLlmWhispererRoutes.js"); - llmWhispererRouter = mod.useLlmWhispererRoutes; -} catch (err) { - if (!isModuleMissing(err)) { - console.error("[llm-whisperer] routes import failed unexpectedly", err); - } -} +const llmWhispererRouter = await loadPlugin(() => + import("../plugins/routes/useLlmWhispererRoutes.js").then( + (m) => m.useLlmWhispererRoutes, + ), +); -let verticalsRouter; -try { - const mod = await import("../plugins/routes/useVerticalsRoutes.js"); - verticalsRouter = mod.useVerticalsRoutes; -} catch (err) { - if (!isModuleMissing(err)) { - console.error("[verticals] routes import failed unexpectedly", err); - } -} +const verticalsRouter = await loadPlugin(() => + import("../plugins/routes/useVerticalsRoutes.js").then( + (m) => m.useVerticalsRoutes, + ), +); function Router() { const location = useLocation(); diff --git a/frontend/src/routes/useMainAppRoutes.jsx b/frontend/src/routes/useMainAppRoutes.jsx index b8234c44b3..2f71ca183f 100644 --- a/frontend/src/routes/useMainAppRoutes.jsx +++ b/frontend/src/routes/useMainAppRoutes.jsx @@ -7,7 +7,7 @@ import { DefaultTriad } from "../components/settings/default-triad/DefaultTriad. import { PlatformSettings } from "../components/settings/platform/PlatformSettings.jsx"; import { deploymentTypes } from "../helpers/GetStaticData.js"; import { lazyNamed } from "../helpers/lazyNamed.js"; -import { isModuleMissing } from "../helpers/pluginLoader.js"; +import { loadPlugin } from "../helpers/pluginLoader.js"; import { lazyPlugin } from "../helpers/pluginRegistry.js"; // Route pages are code-split (via the shared lazyNamed helper) so they are @@ -188,17 +188,12 @@ const MarketplaceStripeConflictPage = lazyPlugin( ); // PRODUCT_NAMES is a data value read synchronously below to decide the route -// tree, so it cannot be lazy — load it with a guarded await (cloud only; OSS -// resolves to the stub and is caught). -let PRODUCT_NAMES = {}; -try { - const mod = await import("../plugins/llm-whisperer/helper.js"); - PRODUCT_NAMES = mod.PRODUCT_NAMES ?? {}; -} catch (err) { - if (!isModuleMissing(err)) { - console.error("[llm-whisperer] helper import failed unexpectedly", err); - } -} +// tree, so it cannot be lazy — load it with loadPlugin (cloud only; OSS +// resolves to the stub and falls back to {}). +const { PRODUCT_NAMES = {} } = await loadPlugin( + () => import("../plugins/llm-whisperer/helper.js"), + {}, +); // NOTE: the old "ReadOnlyReviewPage loaded but ReviewLayout missing" warning // was removed — with lazyPlugin both wrappers are always truthy, so the check diff --git a/frontend/vitest.config.mjs b/frontend/vitest.config.mjs index 128c7f042f..ac4d35be6f 100644 --- a/frontend/vitest.config.mjs +++ b/frontend/vitest.config.mjs @@ -40,8 +40,8 @@ function jsxInJs() { * Mirror of optionalPluginImports() in vite.config.js — the same asymmetry the * note above warns about, in a second place. * - * `src/helpers/GetStaticData.js` does `try { await import("../plugins/...") }`, - * which the build resolves to an empty module when the cloud plugin tree is + * `src/helpers/GetStaticData.js` does + * `await loadPlugin(() => import("../plugins/..."))`, which the build resolves to an empty module when the cloud plugin tree is * absent. Vitest does not read vite.config.js, so in the OSS-only checkout any * test importing a component that reaches GetStaticData failed to COLLECT — * reported as a failed file, not a failed assertion, and easy to read as