+
- React Recipes
+ Multi-Framework Recipes — {FRAMEWORK_LABEL[framework]}
- Sample patterns for building React web apps on Salesforce
+ Sample patterns for Salesforce-Hosted and Externally Hosted apps.
+ React today; Vue and Angular planned.
+ {/* Hosting filter */}
+
+
Hosting:
+
+ {HOSTING_FILTER_OPTIONS.map((option, i) => {
+ const active = filter === option.value;
+ return (
+ activate(option.value)}
+ onKeyDown={(e) => onFilterKeyDown(e, i)}
+ className={cn(
+ 'rounded-md px-3 py-1 text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
+ active
+ ? 'bg-primary text-primary-foreground shadow-sm'
+ : 'text-muted-foreground hover:text-foreground'
+ )}
+ >
+ {option.label}
+
+ );
+ })}
+
+
+
{/* Category tiles */}
- {categories.map(cat => (
-
navigate(cat.to)}
- className="text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-xl"
- >
-
-
-
-
- {cat.name}
-
-
- {getRecipeCount(cat.to)} recipes
-
-
-
- {cat.description}
-
-
-
-
- ))}
+ {visibleCategories.map((cat) => {
+ const hosting = getCategoryHosting(cat.to);
+ const fw = getCategoryFramework(cat.to);
+ const flavors =
+ hosting && fw ? [{ hosting, framework: fw }] : [];
+ return (
+
navigate(cat.to)}
+ className="text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-xl"
+ >
+
+
+
+
+ {cat.name}
+
+
+ {getRecipeCount(cat.to)} recipes
+
+
+ {flavors.length > 0 && (
+
+ )}
+
+ {cat.description}
+
+
+
+
+ );
+ })}
+ {visibleCategories.length === 0 && (
+
+ No categories match this hosting filter yet.
+
+ )}
+
+ {/* Hosting Models block */}
+
+ Hosting Models
+
+ Two ways to ship a framework app on Salesforce. Pick the one that
+ matches where your app lives.
+
+
+ }
+ title={HOSTING_LABEL['salesforce-hosted']}
+ blurb={
+ 'Deploy your app to *.salesforce.app via UI bundles. Best for new apps that want a single deploy step and built-in auth.'
+ }
+ onClick={() => activate('salesforce-hosted')}
+ />
+ }
+ title={HOSTING_LABEL['externally-hosted']}
+ blurb={
+ 'Host on AWS, Heroku, or your own infra; embed via lwc-shell. Best for existing apps and SaaS vendors.'
+ }
+ onClick={() => activate('externally-hosted')}
+ />
+
+
+ Want to run your own server locally with SSL?{' '}
+
+ {EXTERNAL_LINKS.serverRepo.label}
+ {EXTERNAL_LINKS.serverRepo.placeholder ? ' (coming soon)' : ''}
+
+
+ .
+
+
);
}
+
+interface HostingModelCardProps {
+ icon: React.ReactNode;
+ title: string;
+ blurb: string;
+ onClick: () => void;
+}
+
+function HostingModelCard({ icon, title, blurb, onClick }: HostingModelCardProps) {
+ return (
+
+
+
+
+ {icon}
+ {title}
+
+
+ {blurb}
+
+
+
+
+ );
+}
+
diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/pages/Mfe.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/pages/Mfe.tsx
new file mode 100644
index 0000000..e98b7c8
--- /dev/null
+++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/pages/Mfe.tsx
@@ -0,0 +1,466 @@
+/**
+ * Embedding recipes page.
+ *
+ * Each recipe has one or more flavors (hosting × framework). The MFE-style
+ * recipes here ship Externally-Hosted React only today; SF-Hosted and
+ * Vue/Angular flavors are wired as disabled "(soon)" tabs so users can see
+ * the IA without us hiding the future surface.
+ *
+ * Source files: Guest (in mfe-app/), LWC host JS, LWC host HTML — all imported
+ * via ?shiki so the code shown is always current.
+ */
+import { useState } from 'react';
+import { useSearchParams } from 'react-router';
+import { ExternalLink, Info } from 'lucide-react';
+import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
+import {
+ Card,
+ CardHeader,
+ CardTitle,
+ CardDescription,
+ CardContent,
+} from '@/components/ui/card';
+import { cn } from '@/lib/utils';
+import CodeBlock from '@/components/app/CodeBlock';
+import { EXTERNAL_LINKS } from '@/lib/links';
+import {
+ FRAMEWORKS,
+ FRAMEWORK_LABEL,
+ HOSTING_LABEL,
+ HOSTING_PARAM_TO_VALUE,
+ HOSTING_VALUE_TO_PARAM,
+ SOON_LABEL,
+ isFramework,
+ isFrameworkEnabled,
+} from '@/lib/framework';
+import type { Framework, Hosting } from '@/recipeRegistry';
+
+// MFE bridge sources (mfe-app/src/recipes/)
+import basicEmbedMfe from '../../../../../../../mfe-app/src/recipes/BasicEmbed.tsx?shiki';
+import receiveDataMfe from '../../../../../../../mfe-app/src/recipes/ReceiveData.tsx?shiki';
+import sendEventMfe from '../../../../../../../mfe-app/src/recipes/SendEvent.tsx?shiki';
+import autoResizeMfe from '../../../../../../../mfe-app/src/recipes/AutoResize.tsx?shiki';
+import themeTokensMfe from '../../../../../../../mfe-app/src/recipes/ThemeTokens.tsx?shiki';
+import dirtyStateMfe from '../../../../../../../mfe-app/src/recipes/DirtyState.tsx?shiki';
+import graphQLMfe from '../../../../../../../mfe-app/src/recipes/GraphQLBridge.tsx?shiki';
+
+// LWC host JS sources (force-app/main/default/lwc/)
+import basicEmbedJs from '../../../../../default/lwc/mfeBasicEmbed/mfeBasicEmbed.js?shiki=js';
+import receiveDataJs from '../../../../../default/lwc/mfeReceiveData/mfeReceiveData.js?shiki=js';
+import sendEventJs from '../../../../../default/lwc/mfeSendEvent/mfeSendEvent.js?shiki=js';
+import autoResizeJs from '../../../../../default/lwc/mfeAutoResize/mfeAutoResize.js?shiki=js';
+import themeTokensJs from '../../../../../default/lwc/mfeThemeTokens/mfeThemeTokens.js?shiki=js';
+import dirtyStateJs from '../../../../../default/lwc/mfeDirtyState/mfeDirtyState.js?shiki=js';
+import graphQLJs from '../../../../../default/lwc/mfeGraphQL/mfeGraphQL.js?shiki=js';
+
+// LWC host HTML sources (force-app/main/default/lwc/)
+import basicEmbedHtml from '../../../../../default/lwc/mfeBasicEmbed/mfeBasicEmbed.html?shiki=html';
+import receiveDataHtml from '../../../../../default/lwc/mfeReceiveData/mfeReceiveData.html?shiki=html';
+import sendEventHtml from '../../../../../default/lwc/mfeSendEvent/mfeSendEvent.html?shiki=html';
+import autoResizeHtml from '../../../../../default/lwc/mfeAutoResize/mfeAutoResize.html?shiki=html';
+import themeTokensHtml from '../../../../../default/lwc/mfeThemeTokens/mfeThemeTokens.html?shiki=html';
+import dirtyStateHtml from '../../../../../default/lwc/mfeDirtyState/mfeDirtyState.html?shiki=html';
+import graphQLHtml from '../../../../../default/lwc/mfeGraphQL/mfeGraphQL.html?shiki=html';
+
+interface FlavorSources {
+ mfeSource: string;
+ lwcJsSource: string;
+ lwcHtmlSource: string;
+}
+
+interface MfeRecipeFlavor extends FlavorSources {
+ hosting: Hosting;
+ framework: Framework;
+}
+
+interface MfeRecipe {
+ name: string;
+ description: string;
+ flavors: MfeRecipeFlavor[];
+}
+
+const recipes: MfeRecipe[] = [
+ {
+ name: 'Basic Embed',
+ description:
+ 'Minimum viable lwc-shell embed. The host creates the shell, sets src and sandbox, and listens for widget-ready. The MFE uses bridge.isConnected() to detect the embedding context.',
+ flavors: [
+ {
+ hosting: 'externally-hosted',
+ framework: 'react',
+ mfeSource: basicEmbedMfe,
+ lwcJsSource: basicEmbedJs,
+ lwcHtmlSource: basicEmbedHtml,
+ },
+ ],
+ },
+ {
+ name: 'Receive Data',
+ description:
+ "Host pushes data into the MFE via shell.updateData(). The MFE receives it with bridge.addEventListener('data', handler).",
+ flavors: [
+ {
+ hosting: 'externally-hosted',
+ framework: 'react',
+ mfeSource: receiveDataMfe,
+ lwcJsSource: receiveDataJs,
+ lwcHtmlSource: receiveDataHtml,
+ },
+ ],
+ },
+ {
+ name: 'Send Event',
+ description:
+ 'MFE dispatches custom events to the host via bridge.dispatchEvent(). The host catches them as DOM events on the shell element.',
+ flavors: [
+ {
+ hosting: 'externally-hosted',
+ framework: 'react',
+ mfeSource: sendEventMfe,
+ lwcJsSource: sendEventJs,
+ lwcHtmlSource: sendEventHtml,
+ },
+ ],
+ },
+ {
+ name: 'Auto-Resize',
+ description:
+ 'Iframe height follows MFE content via a ResizeObserver inside the iframe. No fixed height on the shell. Cancel the resize event to opt out.',
+ flavors: [
+ {
+ hosting: 'externally-hosted',
+ framework: 'react',
+ mfeSource: autoResizeMfe,
+ lwcJsSource: autoResizeJs,
+ lwcHtmlSource: autoResizeHtml,
+ },
+ ],
+ },
+ {
+ name: 'Theme Tokens',
+ description:
+ 'Salesforce CSS custom properties are sent to the MFE on connect. The MFE applies them to document.documentElement. Call shell.refreshTheme() to re-sync.',
+ flavors: [
+ {
+ hosting: 'externally-hosted',
+ framework: 'react',
+ mfeSource: themeTokensMfe,
+ lwcJsSource: themeTokensJs,
+ lwcHtmlSource: themeTokensHtml,
+ },
+ ],
+ },
+ {
+ name: 'Dirty State',
+ description:
+ 'MFE notifies the host of unsaved changes via trackdirtystate events. The host can show a warning and block navigation.',
+ flavors: [
+ {
+ hosting: 'externally-hosted',
+ framework: 'react',
+ mfeSource: dirtyStateMfe,
+ lwcJsSource: dirtyStateJs,
+ lwcHtmlSource: dirtyStateHtml,
+ },
+ ],
+ },
+ {
+ name: 'GraphQL Bridge',
+ description:
+ 'MFE executes Salesforce GraphQL via bridge.graphql(). The host proxies requests — no allow-same-origin needed. Up to 10 concurrent requests with automatic queuing.',
+ flavors: [
+ {
+ hosting: 'externally-hosted',
+ framework: 'react',
+ mfeSource: graphQLMfe,
+ lwcJsSource: graphQLJs,
+ lwcHtmlSource: graphQLHtml,
+ },
+ ],
+ },
+];
+
+const HOSTINGS: Hosting[] = ['externally-hosted', 'salesforce-hosted'];
+
+function findFlavor(
+ recipe: MfeRecipe,
+ hosting?: Hosting,
+ framework?: Framework
+): MfeRecipeFlavor {
+ if (hosting && framework) {
+ const exact = recipe.flavors.find(
+ (f) => f.hosting === hosting && f.framework === framework
+ );
+ if (exact) return exact;
+ }
+ if (hosting) {
+ const byHosting = recipe.flavors.find((f) => f.hosting === hosting);
+ if (byHosting) return byHosting;
+ }
+ if (framework) {
+ const byFramework = recipe.flavors.find((f) => f.framework === framework);
+ if (byFramework) return byFramework;
+ }
+ return recipe.flavors[0];
+}
+
+export default function Mfe() {
+ const [selectedIndex, setSelectedIndex] = useState(0);
+ const selected = recipes[selectedIndex];
+ const [searchParams, setSearchParams] = useSearchParams();
+
+ // Parse URL flavor params (host, fw) and resolve via fallback. The URL is
+ // corrected via replaceState so an unknown combination doesn't pollute back.
+ const hostParam = searchParams.get('host');
+ const fwParam = searchParams.get('fw');
+ const requestedHosting = hostParam ? HOSTING_PARAM_TO_VALUE[hostParam] : undefined;
+ const requestedFramework =
+ fwParam && isFramework(fwParam) ? fwParam : undefined;
+ const flavor = findFlavor(selected, requestedHosting, requestedFramework);
+
+ function selectFlavor(hosting: Hosting, framework: Framework) {
+ const next = new URLSearchParams(searchParams);
+ next.set('host', HOSTING_VALUE_TO_PARAM[hosting]);
+ next.set('fw', framework);
+ if (searchParams.get('recipe')) {
+ next.set('recipe', searchParams.get('recipe')!);
+ }
+ setSearchParams(next, { replace: true });
+ }
+
+ const isExternallyHosted = flavor.hosting === 'externally-hosted';
+
+ return (
+
+
+
+
Embedding
+
+ {HOSTING_LABEL[flavor.hosting]} · {FRAMEWORK_LABEL[flavor.framework]}
+
+
+
+ An external framework app embedded into Salesforce via{' '}
+ lwc-shell. The Platform SDK (bridge)
+ is the shared contract; a Salesforce-Hosted flavor is planned.
+
+
+
+
+
+ {/* Sidebar */}
+
+
+ {recipes.map((recipe, i) => (
+
+ setSelectedIndex(i)}
+ className={cn(
+ 'w-full text-left px-3 py-2 text-sm rounded-md transition-colors',
+ i === selectedIndex
+ ? 'bg-primary text-primary-foreground font-medium shadow-sm'
+ : 'text-muted-foreground hover:text-foreground hover:bg-accent'
+ )}
+ >
+ {recipe.name}
+
+
+ ))}
+
+
+
+ {/* Recipe card */}
+
+
+ {selected.name}
+ {selected.description}
+
+ {/* Flavor switcher: Hosting */}
+
+ {HOSTINGS.map((h) => {
+ const enabled = recipe_hasFlavor(selected, h, flavor.framework);
+ const active = flavor.hosting === h;
+ return (
+ selectFlavor(h, flavor.framework)}
+ title={
+ enabled
+ ? undefined
+ : `${HOSTING_LABEL[h]} flavor ${SOON_LABEL}`
+ }
+ >
+ {HOSTING_LABEL[h]}
+ {!enabled && }
+
+ );
+ })}
+
+
+ {/* Flavor switcher: Framework */}
+
+ {FRAMEWORKS.map((f) => {
+ const frameworkAvailable = isFrameworkEnabled(f);
+ const flavorAvailable =
+ frameworkAvailable && recipe_hasFlavor(selected, flavor.hosting, f);
+ const active = flavor.framework === f;
+ return (
+ selectFlavor(flavor.hosting, f)}
+ title={
+ flavorAvailable
+ ? undefined
+ : `${FRAMEWORK_LABEL[f]} flavor ${SOON_LABEL}`
+ }
+ >
+ {FRAMEWORK_LABEL[f]}
+ {!flavorAvailable && }
+
+ );
+ })}
+
+
+ {isExternallyHosted && (
+
+ )}
+
+
+
+
+
+ Guest ({FRAMEWORK_LABEL[flavor.framework]})
+
+ Host (LWC JS)
+ Host (LWC HTML)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+function recipe_hasFlavor(
+ recipe: MfeRecipe,
+ hosting: Hosting,
+ framework: Framework
+): boolean {
+ return recipe.flavors.some(
+ (f) => f.hosting === hosting && f.framework === framework
+ );
+}
+
+interface FlavorRowProps {
+ label: string;
+ role: 'tablist' | 'radiogroup';
+ ariaLabel: string;
+ children: React.ReactNode;
+}
+
+function FlavorRow({ label, role, ariaLabel, children }: FlavorRowProps) {
+ return (
+
+
{label}
+
+ {children}
+
+
+ );
+}
+
+interface FlavorButtonProps {
+ active: boolean;
+ enabled: boolean;
+ onClick: () => void;
+ title?: string;
+ children: React.ReactNode;
+}
+
+function FlavorButton({
+ active,
+ enabled,
+ onClick,
+ title,
+ children,
+}: FlavorButtonProps) {
+ return (
+
+ {children}
+
+ );
+}
+
+function SoonTag() {
+ return (
+
+ {SOON_LABEL}
+
+ );
+}
diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipeRegistry.ts b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipeRegistry.ts
index 3a3d7b2..f5d5dcc 100644
--- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipeRegistry.ts
+++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipeRegistry.ts
@@ -1,19 +1,59 @@
/**
* Central registry of all recipes, keyed by category route.
- * Each entry stores the recipe name, description, and the category it belongs to.
- * Used by the Home page for counts and by the global search bar for filtering.
+ *
+ * Each recipe has one or more flavors — a (hosting, framework) pair plus
+ * optional flavor-specific source imports. The first flavor is the default
+ * shown to users; "(soon)" UI states reference flavors that are not yet built.
*/
+export type Hosting = 'salesforce-hosted' | 'externally-hosted';
+export type Framework = 'react' | 'vue' | 'angular';
+
+export interface RecipeFlavor {
+ hosting: Hosting;
+ framework: Framework;
+ /** Source-file imports for code tabs. Shape varies by category; empty when not yet wired. */
+ sources?: Record
;
+ /** Optional flavor-specific description override. */
+ notes?: string;
+}
+
export interface RecipeEntry {
+ /** Stable ID, kebab-case, unique within a category. e.g. "basic-embed" */
+ id: string;
name: string;
description: string;
category: string;
categoryRoute: string;
/** Zero-based index of this recipe within its category page. */
recipeIndex: number;
+ /** At least one. flavors[0] is the default. */
+ flavors: RecipeFlavor[];
+}
+
+const CATEGORY_DEFAULTS: Record = {
+ '/hello': { hosting: 'salesforce-hosted', framework: 'react' },
+ '/read-data': { hosting: 'salesforce-hosted', framework: 'react' },
+ '/modify-data': { hosting: 'salesforce-hosted', framework: 'react' },
+ '/salesforce-apis': { hosting: 'salesforce-hosted', framework: 'react' },
+ '/error-handling': { hosting: 'salesforce-hosted', framework: 'react' },
+ '/styling': { hosting: 'salesforce-hosted', framework: 'react' },
+ '/routing': { hosting: 'salesforce-hosted', framework: 'react' },
+ '/integration': { hosting: 'salesforce-hosted', framework: 'react' },
+ '/mfe': { hosting: 'externally-hosted', framework: 'react' },
+};
+
+interface RawRecipeEntry {
+ category: string;
+ categoryRoute: string;
+ recipeIndex: number;
+ name: string;
+ description: string;
+ id?: string;
+ flavors?: RecipeFlavor[];
}
-export const recipeRegistry: RecipeEntry[] = [
+const rawRecipes: RawRecipeEntry[] = [
// Hello
{
category: 'Hello',
@@ -373,9 +413,192 @@ export const recipeRegistry: RecipeEntry[] = [
description:
'Fetches Accounts, Contacts, and Opportunities in a single aliased GraphQL request.',
},
+
+ // MFE
+ {
+ category: 'Embedding',
+ categoryRoute: '/mfe',
+ recipeIndex: 0,
+ name: 'Basic Embed',
+ description:
+ 'Minimum viable lwc-shell embed. Creates the shell imperatively, sets src and sandbox, and handles widget-ready.',
+ },
+ {
+ category: 'Embedding',
+ categoryRoute: '/mfe',
+ recipeIndex: 1,
+ name: 'Receive Data',
+ description:
+ 'Pushes data from the LWC host into the MFE via shell.updateData(). The MFE receives it with bridge.addEventListener(\'data\', handler).',
+ },
+ {
+ category: 'Embedding',
+ categoryRoute: '/mfe',
+ recipeIndex: 2,
+ name: 'Send Event',
+ description:
+ 'The MFE dispatches custom events to the LWC host via bridge.dispatchEvent(). The host catches them on the shell element.',
+ },
+ {
+ category: 'Embedding',
+ categoryRoute: '/mfe',
+ recipeIndex: 3,
+ name: 'Auto-Resize',
+ description:
+ 'The iframe height adjusts automatically as MFE content grows or shrinks via a ResizeObserver inside the iframe.',
+ },
+ {
+ category: 'Embedding',
+ categoryRoute: '/mfe',
+ recipeIndex: 4,
+ name: 'Theme Tokens',
+ description:
+ 'Salesforce CSS custom properties are sent to the MFE on connect and re-synced on demand via shell.refreshTheme().',
+ },
+ {
+ category: 'Embedding',
+ categoryRoute: '/mfe',
+ recipeIndex: 5,
+ name: 'Dirty State',
+ description:
+ 'The MFE notifies the host of unsaved changes via trackdirtystate events so the host can block navigation.',
+ },
+ {
+ category: 'Embedding',
+ categoryRoute: '/mfe',
+ recipeIndex: 6,
+ name: 'GraphQL Bridge',
+ description:
+ 'The MFE executes Salesforce GraphQL queries proxied through the host via bridge.graphql(). No allow-same-origin needed.',
+ },
];
+function slugify(name: string): string {
+ return name
+ .toLowerCase()
+ .replace(/[()]/g, '')
+ .replace(/[^a-z0-9]+/g, '-')
+ .replace(/^-+|-+$/g, '');
+}
+
+function buildRegistry(): RecipeEntry[] {
+ return rawRecipes.map((entry) => {
+ const flavors: RecipeFlavor[] =
+ entry.flavors ??
+ (() => {
+ const defaults = CATEGORY_DEFAULTS[entry.categoryRoute];
+ if (!defaults) {
+ throw new Error(
+ `Recipe "${entry.name}" in "${entry.categoryRoute}" has no category default and no explicit flavors.`
+ );
+ }
+ return [{ hosting: defaults.hosting, framework: defaults.framework }];
+ })();
+
+ if (flavors.length === 0) {
+ throw new Error(
+ `Recipe "${entry.name}" in "${entry.categoryRoute}" must have at least one flavor.`
+ );
+ }
+
+ // Validate (hosting, framework) pairs are unique within a recipe.
+ const seen = new Set();
+ for (const f of flavors) {
+ const key = `${f.hosting}|${f.framework}`;
+ if (seen.has(key)) {
+ throw new Error(
+ `Recipe "${entry.name}" has duplicate flavor (${f.hosting}, ${f.framework}).`
+ );
+ }
+ seen.add(key);
+ }
+
+ return {
+ id: entry.id ?? slugify(entry.name),
+ name: entry.name,
+ description: entry.description,
+ category: entry.category,
+ categoryRoute: entry.categoryRoute,
+ recipeIndex: entry.recipeIndex,
+ flavors,
+ };
+ });
+}
+
+export const recipeRegistry: RecipeEntry[] = buildRegistry();
+
/** Returns the number of recipes for a given category route (e.g. "/hello"). */
export function getRecipeCount(categoryRoute: string): number {
return recipeRegistry.filter((r) => r.categoryRoute === categoryRoute).length;
}
+
+/** Returns the default hosting for a category, or undefined if the category is unknown. */
+export function getCategoryHosting(categoryRoute: string): Hosting | undefined {
+ return CATEGORY_DEFAULTS[categoryRoute]?.hosting;
+}
+
+/** Returns the default framework for a category, or undefined if the category is unknown. */
+export function getCategoryFramework(categoryRoute: string): Framework | undefined {
+ return CATEGORY_DEFAULTS[categoryRoute]?.framework;
+}
+
+/** Look up a recipe by category route + id. */
+export function getRecipe(categoryRoute: string, id: string): RecipeEntry | undefined {
+ return recipeRegistry.find(
+ (r) => r.categoryRoute === categoryRoute && r.id === id
+ );
+}
+
+/** List recipes, optionally narrowed to those that have at least one matching flavor. */
+export function listRecipes(filter?: {
+ hosting?: Hosting;
+ framework?: Framework;
+}): RecipeEntry[] {
+ if (!filter || (!filter.hosting && !filter.framework)) return recipeRegistry;
+ return recipeRegistry.filter((r) =>
+ r.flavors.some(
+ (f) =>
+ (!filter.hosting || f.hosting === filter.hosting) &&
+ (!filter.framework || f.framework === filter.framework)
+ )
+ );
+}
+
+/** True if the recipe declares a flavor exactly matching (hosting, framework). */
+export function hasFlavor(
+ recipe: RecipeEntry,
+ hosting: Hosting,
+ framework: Framework
+): boolean {
+ return recipe.flavors.some(
+ (f) => f.hosting === hosting && f.framework === framework
+ );
+}
+
+/**
+ * Resolve the best-matching flavor for the requested (hosting, framework).
+ * Fallback order: exact match → hosting only → framework only → flavors[0].
+ * Callers that care which axis matched should compare the returned flavor's
+ * fields to what they requested.
+ */
+export function resolveFlavor(
+ recipe: RecipeEntry,
+ hosting?: Hosting,
+ framework?: Framework
+): RecipeFlavor {
+ if (hosting && framework) {
+ const exact = recipe.flavors.find(
+ (f) => f.hosting === hosting && f.framework === framework
+ );
+ if (exact) return exact;
+ }
+ if (hosting) {
+ const byHosting = recipe.flavors.find((f) => f.hosting === hosting);
+ if (byHosting) return byHosting;
+ }
+ if (framework) {
+ const byFramework = recipe.flavors.find((f) => f.framework === framework);
+ if (byFramework) return byFramework;
+ }
+ return recipe.flavors[0];
+}
diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/README.md b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/README.md
index d01bdfa..1714a05 100644
--- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/README.md
+++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/README.md
@@ -1,6 +1,8 @@
-# React Recipes Catalog
+# Multi-Framework Recipes Catalog (React)
-React Recipes are self-contained, single-file examples that teach one concept at a time. Every recipe inlines its GraphQL queries, mutations, types, and SDK calls so you can read the entire pattern without jumping between files. Open any `.tsx` file in `src/recipes/` and everything you need is right there.
+Self-contained, single-file examples that teach one concept at a time. Every recipe inlines its GraphQL queries, mutations, types, and SDK calls so you can read the entire pattern without jumping between files. Open any `.tsx` file in `src/recipes/` and everything you need is right there.
+
+Each recipe is tagged by **hosting mode** (Salesforce-Hosted or Externally Hosted) and **framework** (React today; Vue/Angular planned). Categories 1–8 are Salesforce-Hosted React recipes. Category 9 (MFE) is Externally Hosted React — the framework app runs on its own server and embeds into Salesforce via `lwc-shell`.
## Recommended Learning Path
@@ -14,6 +16,7 @@ Work through the categories in this order. Each builds on concepts from the prev
6. **Routing** -- React Router in UI Bundles: Link, NavLink, route parameters, nested routes, programmatic navigation
7. **Styling** -- CSS approaches: SLDS utility classes, shadcn/ui + Tailwind, Design System React components
8. **Integration** -- End-to-end patterns combining multiple APIs and React features
+9. **MFE (Externally Hosted)** -- Embed an external React app into Salesforce Lightning via `lwc-shell`. Covers the Platform SDK surface: connection detection, receiving host data, dispatching events, auto-resize, theme tokens, dirty state, and GraphQL proxying. Requires the `mfe-app/` dev server running at `localhost:4300` — see the repo README for setup steps.
## Full Recipe Table
@@ -63,3 +66,10 @@ Work through the categories in this order. Each builds on concepts from the prev
| Styling | IconsDSR | Same icons as IconsSLDS via the Icon component from design-system-react. |
| Integration | SearchableAccountList | A controlled search input drives a GraphQL variable that filters Accounts by name with debounce. |
| Integration | DashboardAliasedQueries | Fetches Accounts, Contacts, and Opportunities in a single GraphQL request using aliases. |
+| MFE | BasicEmbed | Minimum viable lwc-shell embed. The MFE uses `bridge.isConnected()` to detect the embedding context. |
+| MFE | ReceiveData | Host pushes data into the MFE via `shell.updateData()`. MFE receives it with `bridge.addEventListener('data', handler)`. |
+| MFE | SendEvent | MFE dispatches custom events to the host via `bridge.dispatchEvent()`. Host catches them on the shell element. |
+| MFE | AutoResize | iframe height adjusts automatically as MFE content grows or shrinks via a ResizeObserver inside the iframe. |
+| MFE | ThemeTokens | Salesforce CSS custom properties sent to the MFE on connect and re-synced on demand via `shell.refreshTheme()`. |
+| MFE | DirtyState | MFE notifies the host of unsaved changes via `trackdirtystate` events so the host can block navigation. |
+| MFE | GraphQLBridge | MFE executes Salesforce GraphQL queries proxied through the host via `bridge.graphql()`. No `allow-same-origin` needed. |
diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/routes.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/routes.tsx
index db5113b..24b17f0 100644
--- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/routes.tsx
+++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/routes.tsx
@@ -11,6 +11,7 @@ import ErrorHandling from './pages/ErrorHandling';
import Styling from './pages/Styling';
import Routing, { RouteParametersPage, NestedRoutesPage } from './pages/Routing';
import Integration from './pages/Integration';
+import Mfe from './pages/Mfe';
import { RouteParametersDetail } from './recipes/routing/RouteParameters';
import NestedRoutes, {
NestedRoutesIndex,
@@ -91,6 +92,11 @@ export const routes: RouteObject[] = [
element: ,
handle: { showInNavigation: true, label: 'Integration' },
},
+ {
+ path: 'mfe',
+ element: ,
+ handle: { showInNavigation: true, label: 'Mfe' },
+ },
{
path: '*',
element: ,
diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/vite-env.d.ts b/force-app/main/react-recipes/uiBundles/reactRecipes/vite-env.d.ts
index 4a2531c..6a03bb4 100644
--- a/force-app/main/react-recipes/uiBundles/reactRecipes/vite-env.d.ts
+++ b/force-app/main/react-recipes/uiBundles/reactRecipes/vite-env.d.ts
@@ -4,3 +4,13 @@ declare module '*?shiki' {
const html: string;
export default html;
}
+
+declare module '*?shiki=js' {
+ const html: string;
+ export default html;
+}
+
+declare module '*?shiki=html' {
+ const html: string;
+ export default html;
+}
diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/vite-plugin-shiki.ts b/force-app/main/react-recipes/uiBundles/reactRecipes/vite-plugin-shiki.ts
index 86f8a70..d16519c 100644
--- a/force-app/main/react-recipes/uiBundles/reactRecipes/vite-plugin-shiki.ts
+++ b/force-app/main/react-recipes/uiBundles/reactRecipes/vite-plugin-shiki.ts
@@ -3,9 +3,11 @@
*
* Import any file with `?shiki` to get pre-highlighted HTML:
*
- * import html from './MyComponent.tsx?shiki';
+ * import html from './MyComponent.tsx?shiki'; // tsx + twoslash
+ * import html from './Component.js?shiki=js'; // js, no twoslash
+ * import html from './Component.html?shiki=html'; // html
*
- * Uses shiki with material-theme-darker and twoslash for hover types.
+ * Uses shiki with material-theme-darker. Only tsx uses twoslash for hover types.
*/
import { readFile } from 'node:fs/promises';
import { createHighlighter, type Highlighter } from 'shiki';
@@ -16,8 +18,6 @@ import {
import type { Plugin } from 'vite';
import baseTheme from 'shiki/themes/material-theme-darker.mjs';
-const SHIKI_QUERY = 'shiki';
-
// Brighten comments (#545454 is too dim on the dark background)
const theme = {
...baseTheme,
@@ -58,7 +58,7 @@ function getHighlighter() {
if (!highlighterPromise) {
highlighterPromise = createHighlighter({
themes: [theme],
- langs: ['tsx', 'graphql', gqlInjection as any],
+ langs: ['tsx', 'javascript', 'html', 'graphql', gqlInjection as any],
});
}
return highlighterPromise;
@@ -87,22 +87,32 @@ export default function shikiPlugin(): Plugin {
async load(id) {
const [filePath, query] = id.split('?');
- if (query !== SHIKI_QUERY) return;
+ if (!query?.startsWith('shiki')) return;
+ // ?shiki → tsx (default, uses twoslash)
+ // ?shiki=js → javascript
+ // ?shiki=html → html
+ const lang = query === 'shiki' ? 'tsx' : query.slice('shiki='.length);
const source = await readFile(filePath, 'utf-8');
const highlighter = await getHighlighter();
let html: string;
- try {
- html = highlighter.codeToHtml(`// @noErrors\n${source}`, {
- lang: 'tsx',
- theme: 'material-theme-darker',
- transformers: [twoslash],
- });
- } catch {
- // Fall back to plain highlighting if twoslash fails
+ if (lang === 'tsx') {
+ try {
+ html = highlighter.codeToHtml(`// @noErrors\n${source}`, {
+ lang: 'tsx',
+ theme: 'material-theme-darker',
+ transformers: [twoslash],
+ });
+ } catch {
+ html = highlighter.codeToHtml(source, {
+ lang: 'tsx',
+ theme: 'material-theme-darker',
+ });
+ }
+ } else {
html = highlighter.codeToHtml(source, {
- lang: 'tsx',
+ lang,
theme: 'material-theme-darker',
});
}
diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/vite.config.ts b/force-app/main/react-recipes/uiBundles/reactRecipes/vite.config.ts
index ba28698..6f2bd51 100644
--- a/force-app/main/react-recipes/uiBundles/reactRecipes/vite.config.ts
+++ b/force-app/main/react-recipes/uiBundles/reactRecipes/vite.config.ts
@@ -34,6 +34,14 @@ export default defineConfig(({ mode }) => {
: []),
] as import('vite').PluginOption[],
+ // Allow reading source files from mfe-app/ and force-app/main/default/lwc/
+ // for the MFE recipes ?shiki source imports.
+ server: {
+ fs: {
+ allow: [resolve(__dirname, '../../../../..')],
+ },
+ },
+
// design-system-react ships CJS — pre-bundle for Vite's ESM dev server
optimizeDeps: {
include: ['@salesforce/design-system-react'],
diff --git a/mfe-app/index.html b/mfe-app/index.html
new file mode 100644
index 0000000..6f6a2d9
--- /dev/null
+++ b/mfe-app/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ MFE App
+
+
+
+
+
+
diff --git a/mfe-app/package.json b/mfe-app/package.json
new file mode 100644
index 0000000..f5c01ce
--- /dev/null
+++ b/mfe-app/package.json
@@ -0,0 +1,32 @@
+{
+ "name": "mfe-app",
+ "private": true,
+ "version": "1.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc -b && vite build",
+ "preview": "vite preview",
+ "lint": "eslint ."
+ },
+ "dependencies": {
+ "@salesforce/experimental-mfe-bridge": "2.2.1-rc.1",
+ "react": "^19.2.0",
+ "react-dom": "^19.2.0",
+ "react-router-dom": "^7.9.5"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.39.1",
+ "@types/node": "^25.0.9",
+ "@types/react": "^19.2.2",
+ "@types/react-dom": "^19.2.2",
+ "@vitejs/plugin-react": "^5.1.0",
+ "eslint": "^9.39.1",
+ "eslint-plugin-react-hooks": "^5.2.0",
+ "eslint-plugin-react-refresh": "^0.4.24",
+ "globals": "^16.5.0",
+ "typescript": "~5.9.3",
+ "typescript-eslint": "^8.46.3",
+ "vite": "^7.2.2"
+ }
+}
diff --git a/mfe-app/src/App.tsx b/mfe-app/src/App.tsx
new file mode 100644
index 0000000..e4bd465
--- /dev/null
+++ b/mfe-app/src/App.tsx
@@ -0,0 +1,52 @@
+import { BrowserRouter, Routes, Route } from 'react-router-dom';
+import BasicEmbed from './recipes/BasicEmbed';
+import ReceiveData from './recipes/ReceiveData';
+import SendEvent from './recipes/SendEvent';
+import AutoResize from './recipes/AutoResize';
+import ThemeTokens from './recipes/ThemeTokens';
+import DirtyState from './recipes/DirtyState';
+import GraphQLBridge from './recipes/GraphQLBridge';
+
+function Home() {
+ return (
+
+
MFE App
+
+ External MFE application for multiframework-recipes. Each route demonstrates one
+ bridge pattern. Open this app embedded inside a Salesforce Lightning component.
+
+
+
Available routes
+
+ /basic-embed
+ /receive-data
+ /send-event
+ /auto-resize
+ /theme-tokens
+ /dirty-state
+ /graphql-bridge
+
+
+
+ );
+}
+
+function App() {
+ return (
+
+
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+ );
+}
+
+export default App;
diff --git a/mfe-app/src/index.css b/mfe-app/src/index.css
new file mode 100644
index 0000000..ba350a5
--- /dev/null
+++ b/mfe-app/src/index.css
@@ -0,0 +1,126 @@
+:root {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
+ Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', Arial,
+ sans-serif;
+ line-height: 1.5;
+ font-weight: 400;
+ color: #213547;
+ background-color: #ffffff;
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+}
+
+body {
+ margin: 0;
+ min-width: 320px;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+.recipe-container {
+ padding: 16px;
+ font-size: 14px;
+}
+
+.recipe-title {
+ font-size: 16px;
+ font-weight: 600;
+ margin: 0 0 4px;
+ color: #0a7cae;
+}
+
+.recipe-description {
+ font-size: 12px;
+ color: #666;
+ margin: 0 0 16px;
+}
+
+.recipe-card {
+ border: 1px solid #e5e7eb;
+ border-radius: 8px;
+ padding: 16px;
+ background: #fff;
+}
+
+.recipe-label {
+ font-size: 11px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ color: #888;
+ margin: 0 0 4px;
+}
+
+.recipe-value {
+ font-size: 14px;
+ color: #213547;
+ margin: 0 0 12px;
+}
+
+.recipe-badge {
+ display: inline-block;
+ padding: 2px 10px;
+ border-radius: 12px;
+ font-size: 12px;
+ font-weight: 600;
+}
+
+.badge-green { background: #d1fae5; color: #065f46; }
+.badge-yellow { background: #fef3c7; color: #92400e; }
+.badge-red { background: #fee2e2; color: #991b1b; }
+.badge-blue { background: #dbeafe; color: #1e40af; }
+
+.recipe-btn {
+ display: inline-block;
+ padding: 6px 14px;
+ border-radius: 6px;
+ border: none;
+ font-size: 13px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: opacity 0.15s;
+}
+
+.recipe-btn:hover { opacity: 0.85; }
+.recipe-btn:disabled { opacity: 0.5; cursor: not-allowed; }
+
+.recipe-btn-primary { background: #0a7cae; color: #fff; }
+.recipe-btn-secondary { background: #e5e7eb; color: #213547; }
+.recipe-btn-danger { background: #dc2626; color: #fff; }
+
+.recipe-input {
+ width: 100%;
+ padding: 8px 10px;
+ border: 1px solid #d1d5db;
+ border-radius: 6px;
+ font-size: 13px;
+ outline: none;
+}
+
+.recipe-input:focus { border-color: #0a7cae; }
+
+.recipe-alert {
+ padding: 10px 14px;
+ border-radius: 6px;
+ font-size: 13px;
+ margin-bottom: 12px;
+}
+
+.alert-warning { background: #fef3c7; color: #92400e; border: 1px solid #fcd34d; }
+.alert-error { background: #fee2e2; color: #991b1b; border: 1px solid #fca5a5; }
+.alert-success { background: #d1fae5; color: #065f46; border: 1px solid #6ee7b7; }
+.alert-info { background: #dbeafe; color: #1e40af; border: 1px solid #93c5fd; }
+
+.status-dot {
+ display: inline-block;
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ margin-right: 6px;
+}
+
+.dot-green { background: #10b981; }
+.dot-gray { background: #9ca3af; }
diff --git a/mfe-app/src/main.tsx b/mfe-app/src/main.tsx
new file mode 100644
index 0000000..7825095
--- /dev/null
+++ b/mfe-app/src/main.tsx
@@ -0,0 +1,10 @@
+import { StrictMode } from 'react';
+import { createRoot } from 'react-dom/client';
+import './index.css';
+import App from './App.tsx';
+
+createRoot(document.getElementById('root')!).render(
+
+
+ ,
+);
diff --git a/mfe-app/src/recipes/AutoResize.tsx b/mfe-app/src/recipes/AutoResize.tsx
new file mode 100644
index 0000000..369910b
--- /dev/null
+++ b/mfe-app/src/recipes/AutoResize.tsx
@@ -0,0 +1,87 @@
+/**
+ * Auto-Resize
+ *
+ * Demonstrates iframe height auto-adjustment. As content is added or removed
+ * the lwc-shell on the host side receives resize events and updates the iframe
+ * height automatically — no fixed height is set.
+ *
+ * Key concept: the bridge reports height changes via a ResizeObserver on
+ * document.body. This happens automatically when using
+ * @salesforce/experimental-mfe-bridge — no explicit setup needed in the app.
+ * Simply add or remove content and the iframe height follows.
+ *
+ * @see ThemeTokens — receiving Salesforce design tokens
+ */
+import { useState } from 'react';
+
+interface Item {
+ id: number;
+ text: string;
+}
+
+let nextId = 1;
+
+function makeItem(): Item {
+ return { id: nextId++, text: `Item ${nextId - 1} — added at ${new Date().toLocaleTimeString()}` };
+}
+
+export default function AutoResize() {
+ const [items, setItems] = useState- ([makeItem(), makeItem()]);
+
+ function addItem() {
+ setItems(prev => [...prev, makeItem()]);
+ }
+
+ function removeItem(id: number) {
+ setItems(prev => prev.filter(item => item.id !== id));
+ }
+
+ return (
+
+
Auto-Resize
+
+ Add or remove items — the Salesforce iframe height adjusts automatically.
+ No fixed height is set on the host. The bridge reports content height via
+ a ResizeObserver.
+
+
+
+
+ + Add item
+
+ setItems([])}
+ disabled={items.length === 0}
+ >
+ Clear all
+
+
+
+
+
{items.length} item{items.length !== 1 ? 's' : ''}
+ {items.length === 0 ? (
+
No items — iframe should be at minimum height.
+ ) : (
+
+ {items.map(item => (
+
+ {item.text}
+ removeItem(item.id)}
+ >
+ remove
+
+
+ ))}
+
+ )}
+
+
+ );
+}
diff --git a/mfe-app/src/recipes/BasicEmbed.tsx b/mfe-app/src/recipes/BasicEmbed.tsx
new file mode 100644
index 0000000..38af6a0
--- /dev/null
+++ b/mfe-app/src/recipes/BasicEmbed.tsx
@@ -0,0 +1,51 @@
+/**
+ * Basic Embed
+ *
+ * The minimum viable MFE. Checks whether the bridge is connected on mount
+ * and displays the connection status.
+ *
+ * On the Salesforce side the host LWC creates an pointing at
+ * this route. The bridge auto-initialises when this page loads inside that
+ * iframe — no explicit setup is needed.
+ *
+ * Key concept: bridge.isConnected() returns true only when running inside a
+ * Salesforce lwc-shell iframe. Use it to detect the embedding context and
+ * render accordingly.
+ *
+ * @see ReceiveData — receiving host data via the bridge
+ */
+import { useEffect, useState } from 'react';
+import bridge from '@salesforce/experimental-mfe-bridge';
+
+export default function BasicEmbed() {
+ const [connected, setConnected] = useState(false);
+
+ useEffect(() => {
+ // bridge.isConnected() is true when running inside an lwc-shell iframe.
+ // Outside Salesforce (plain browser tab) it returns false — useful for
+ // local development without a Salesforce org.
+ setConnected(bridge.isConnected());
+ }, []);
+
+ return (
+
+
Basic Embed
+
+ Detects whether this React app is running inside a Salesforce lwc-shell iframe.
+
+
+
+
Bridge status
+
+
+ {connected ? 'Connected — running inside Salesforce' : 'Not connected — running standalone'}
+
+
+
Instance ID
+
+ {connected ? bridge.instanceId : '—'}
+
+
+
+ );
+}
diff --git a/mfe-app/src/recipes/DirtyState.tsx b/mfe-app/src/recipes/DirtyState.tsx
new file mode 100644
index 0000000..8cb2c3e
--- /dev/null
+++ b/mfe-app/src/recipes/DirtyState.tsx
@@ -0,0 +1,129 @@
+/**
+ * Dirty State
+ *
+ * Notifies the Salesforce host when this MFE has unsaved changes. The host
+ * listens for the 'trackdirtystate' event on the shell element and can use
+ * it to block tab navigation or show a "You have unsaved changes" warning.
+ *
+ * Key concept: dispatch a 'trackdirtystate' CustomEvent with
+ * { isDirty: boolean, label: string } whenever the form state changes.
+ * Set isDirty: false when the user saves or discards changes.
+ *
+ * @see SendEvent — dispatching generic events to the host
+ */
+import { useState, useEffect } from 'react';
+import bridge from '@salesforce/experimental-mfe-bridge';
+
+interface FormState {
+ name: string;
+ amount: string;
+ notes: string;
+}
+
+const INITIAL: FormState = { name: '', amount: '', notes: '' };
+
+function isDirtyCheck(current: FormState, saved: FormState) {
+ return JSON.stringify(current) !== JSON.stringify(saved);
+}
+
+export default function DirtyState() {
+ const [form, setForm] = useState(INITIAL);
+ const [saved, setSaved] = useState(INITIAL);
+ const [isDirty, setIsDirty] = useState(false);
+
+ useEffect(() => {
+ const dirty = isDirtyCheck(form, saved);
+ setIsDirty(dirty);
+
+ // Notify the Salesforce host whenever dirty state changes.
+ // The host uses this to show a warning before the user navigates away.
+ bridge.dispatchEvent(
+ new CustomEvent('trackdirtystate', {
+ detail: {
+ isDirty: dirty,
+ label: dirty ? 'Loan application has unsaved changes' : '',
+ },
+ }),
+ );
+ }, [form, saved]);
+
+ function handleChange(field: keyof FormState, value: string) {
+ setForm(prev => ({ ...prev, [field]: value }));
+ }
+
+ function handleSave() {
+ setSaved(form);
+ }
+
+ function handleDiscard() {
+ setForm(saved);
+ }
+
+ return (
+
+
Dirty State
+
+ Edit the form — the host is notified of unsaved changes via{' '}
+ bridge.dispatchEvent('trackdirtystate'). The host can block
+ navigation until the user saves or discards.
+
+
+ {isDirty && (
+
+ Unsaved changes — the Salesforce host has been notified.
+
+ )}
+
+ {!isDirty && saved.name && (
+
Changes saved.
+ )}
+
+
+
+
+
+ Save
+
+
+ Discard
+
+
+
+ );
+}
diff --git a/mfe-app/src/recipes/GraphQLBridge.tsx b/mfe-app/src/recipes/GraphQLBridge.tsx
new file mode 100644
index 0000000..3db9a00
--- /dev/null
+++ b/mfe-app/src/recipes/GraphQLBridge.tsx
@@ -0,0 +1,124 @@
+/**
+ * GraphQL Bridge
+ *
+ * Executes a Salesforce GraphQL query from inside the MFE iframe. The bridge
+ * proxies the request through the host LWC — the MFE never needs
+ * allow-same-origin. Up to 10 requests can be in-flight simultaneously;
+ * extras are queued automatically.
+ *
+ * Key concept: bridge.graphql({ query, variables }) returns a Promise that
+ * resolves with the same shape as a direct GraphQL response. The host must
+ * have the graphql bridge enabled (the mfeGraphQL LWC uses it automatically
+ * via vendorLwcShell).
+ *
+ * @see ReceiveData — simpler host → MFE data flow without GraphQL
+ */
+import { useState } from 'react';
+import bridge from '@salesforce/experimental-mfe-bridge';
+
+const QUERY = `
+ query MfeContacts {
+ uiapi {
+ query {
+ Contact(first: 5, orderBy: { Name: { order: ASC } }) {
+ edges {
+ node {
+ Id
+ Name { value }
+ Title { value }
+ }
+ }
+ }
+ }
+ }
+ }
+`;
+
+interface ContactNode {
+ Id: string;
+ Name: { value: string | null };
+ Title: { value: string | null };
+}
+
+interface QueryResult {
+ data?: {
+ uiapi?: {
+ query?: {
+ Contact?: {
+ edges?: Array<{ node: ContactNode }>;
+ };
+ };
+ };
+ };
+ errors?: Array<{ message: string }>;
+}
+
+export default function GraphQLBridge() {
+ const [contacts, setContacts] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState();
+
+ async function fetchContacts() {
+ if (!bridge.isConnected()) {
+ setError('Bridge not connected — embed this app inside the mfeGraphQL LWC to execute queries.');
+ return;
+ }
+
+ setLoading(true);
+ setError(undefined);
+
+ try {
+ // bridge.graphql() proxies the query through the host LWC.
+ // The MFE never directly calls Salesforce APIs — the host acts as the proxy.
+ const result = (await bridge.graphql({ query: QUERY })) as QueryResult;
+
+ if (result.errors?.length) {
+ throw new Error(result.errors.map(e => e.message).join('; '));
+ }
+
+ const edges = result.data?.uiapi?.query?.Contact?.edges ?? [];
+ setContacts(edges.map(e => e.node));
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Query failed');
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ return (
+
+
GraphQL Bridge
+
+ Executes a Salesforce GraphQL query via bridge.graphql(). The
+ host proxies the request — no allow-same-origin needed.
+
+
+
+ {loading ? 'Querying…' : 'Fetch contacts'}
+
+
+ {error &&
{error}
}
+
+ {!error && contacts.length > 0 && (
+
+
{contacts.length} contact{contacts.length !== 1 ? 's' : ''}
+
+ {contacts.map(c => (
+
+ {c.Name.value ?? 'Unknown'}
+ {c.Title.value && (
+ {c.Title.value}
+ )}
+
+ ))}
+
+
+ )}
+
+ );
+}
diff --git a/mfe-app/src/recipes/ReceiveData.tsx b/mfe-app/src/recipes/ReceiveData.tsx
new file mode 100644
index 0000000..1b1f84e
--- /dev/null
+++ b/mfe-app/src/recipes/ReceiveData.tsx
@@ -0,0 +1,75 @@
+/**
+ * Receive Data
+ *
+ * Listens for data pushed from the Salesforce LWC host via updateData().
+ * The host calls shell.updateData({ recordId, name }) after @wire(getRecord)
+ * resolves — this component receives and displays that payload.
+ *
+ * Key concept: bridge.addEventListener('data', handler) fires every time
+ * the host calls updateData(). The event detail is the exact object passed
+ * to updateData(). Always remove the listener in the cleanup function to
+ * prevent stale handlers after unmount.
+ *
+ * @see SendEvent — dispatching events back to the host
+ */
+import { useEffect, useState } from 'react';
+import bridge from '@salesforce/experimental-mfe-bridge';
+
+interface HostData {
+ recordId?: string;
+ name?: string;
+ [key: string]: unknown;
+}
+
+export default function ReceiveData() {
+ const [hostData, setHostData] = useState({});
+ const [updateCount, setUpdateCount] = useState(0);
+
+ useEffect(() => {
+ const handleData = (e: Event) => {
+ const detail = (e as CustomEvent).detail ?? {};
+ setHostData(detail);
+ setUpdateCount(c => c + 1);
+ };
+
+ // 'data' fires whenever the host calls shell.updateData(payload)
+ bridge.addEventListener('data', handleData);
+ return () => bridge.removeEventListener('data', handleData);
+ }, []);
+
+ const hasData = Object.keys(hostData).length > 0;
+
+ return (
+
+
Receive Data
+
+ Displays data pushed from the Salesforce host via{' '}
+ shell.updateData(). Interact with the host component to
+ trigger an update.
+
+
+ {!bridge.isConnected() && (
+
+ Running standalone — no host data will arrive. Embed this app in the
+ mfeReceiveData LWC to see live data.
+
+ )}
+
+
+
Updates received
+
{updateCount}
+
+
Latest payload
+ {hasData ? (
+
+ {JSON.stringify(hostData, null, 2)}
+
+ ) : (
+
+ Waiting for host data…
+
+ )}
+
+
+ );
+}
diff --git a/mfe-app/src/recipes/SendEvent.tsx b/mfe-app/src/recipes/SendEvent.tsx
new file mode 100644
index 0000000..38c1314
--- /dev/null
+++ b/mfe-app/src/recipes/SendEvent.tsx
@@ -0,0 +1,89 @@
+/**
+ * Send Event
+ *
+ * Dispatches a custom event from the MFE back to the Salesforce LWC host.
+ * The host listens on the shell element: shell.addEventListener('mfe-action', handler).
+ *
+ * Key concept: bridge.dispatchEvent(new CustomEvent(type, { detail })) sends the
+ * event through lwc-shell to the parent LWC. The host catches it as a DOM event
+ * on the shell element. Any serialisable object is valid as the detail payload.
+ *
+ * @see ReceiveData — receiving data pushed from the host
+ */
+import { useState } from 'react';
+import bridge from '@salesforce/experimental-mfe-bridge';
+
+const ACTIONS = ['approve', 'reject', 'request-docs', 'escalate'] as const;
+type Action = (typeof ACTIONS)[number];
+
+interface EventLog {
+ action: Action;
+ timestamp: string;
+}
+
+export default function SendEvent() {
+ const [log, setLog] = useState([]);
+
+ function handleAction(action: Action) {
+ // Dispatch a custom event that bubbles up to the LWC host.
+ // The host listens: shell.addEventListener('mfe-action', handler)
+ bridge.dispatchEvent(
+ new CustomEvent('mfe-action', {
+ detail: { action, timestamp: Date.now() },
+ }),
+ );
+
+ setLog(prev => [
+ { action, timestamp: new Date().toLocaleTimeString() },
+ ...prev.slice(0, 9),
+ ]);
+ }
+
+ return (
+
+
Send Event
+
+ Dispatches custom events to the Salesforce LWC host via{' '}
+ bridge.dispatchEvent(). The host receives them on the shell
+ element.
+
+
+ {!bridge.isConnected() && (
+
+ Running standalone — events are dispatched but no host is listening.
+
+ )}
+
+
+
Actions
+
+ {ACTIONS.map(action => (
+ handleAction(action)}
+ >
+ {action}
+
+ ))}
+
+
+
+ {log.length > 0 && (
+
+
Dispatched events
+
+ {log.map((entry, i) => (
+
+ {entry.action}
+
+ {entry.timestamp}
+
+
+ ))}
+
+
+ )}
+
+ );
+}
diff --git a/mfe-app/src/recipes/ThemeTokens.tsx b/mfe-app/src/recipes/ThemeTokens.tsx
new file mode 100644
index 0000000..e73613c
--- /dev/null
+++ b/mfe-app/src/recipes/ThemeTokens.tsx
@@ -0,0 +1,90 @@
+/**
+ * Theme Tokens
+ *
+ * Receives Salesforce CSS custom properties (design tokens) from the host
+ * and applies them to the UI. The bridge sends all --slds-* properties from
+ * the lwc-shell element automatically on connect, and again whenever the host
+ * calls shell.refreshTheme().
+ *
+ * Key concept: bridge.addEventListener('theme', handler) receives a map of
+ * CSS custom property names to values. Apply them to document.documentElement
+ * via setProperty() so they cascade to the whole app. The bridge does this
+ * automatically — this recipe makes the process explicit and visible.
+ *
+ * @see DirtyState — notifying the host about unsaved changes
+ */
+import { useEffect, useState } from 'react';
+import bridge from '@salesforce/experimental-mfe-bridge';
+
+interface ThemeData {
+ [property: string]: string;
+}
+
+export default function ThemeTokens() {
+ const [tokens, setTokens] = useState({});
+ const [syncCount, setSyncCount] = useState(0);
+
+ useEffect(() => {
+ const handleTheme = (e: Event) => {
+ const detail = (e as CustomEvent).detail ?? {};
+
+ // Apply every CSS custom property to the document root so they
+ // cascade to all components in this MFE.
+ Object.entries(detail).forEach(([prop, value]) => {
+ document.documentElement.style.setProperty(prop, value);
+ });
+
+ setTokens(detail);
+ setSyncCount(c => c + 1);
+ };
+
+ // 'theme' fires on connect and whenever the host calls shell.refreshTheme()
+ bridge.addEventListener('theme', handleTheme);
+ return () => bridge.removeEventListener('theme', handleTheme);
+ }, []);
+
+ const tokenEntries = Object.entries(tokens).slice(0, 20);
+
+ return (
+
+
Theme Tokens
+
+ Displays Salesforce CSS custom properties received from the host.
+ The host sends them automatically; call{' '}
+ shell.refreshTheme() to re-sync after a theme change.
+
+
+ {!bridge.isConnected() && (
+
+ Running standalone — no theme data will arrive from a host.
+
+ )}
+
+
+
Syncs received: {syncCount}
+
Tokens (first 20 of {Object.keys(tokens).length})
+
+ {tokenEntries.length === 0 ? (
+
Waiting for theme data…
+ ) : (
+
+
+
+ Property
+ Value
+
+
+
+ {tokenEntries.map(([prop, value]) => (
+
+ {prop}
+ {value}
+
+ ))}
+
+
+ )}
+
+
+ );
+}
diff --git a/mfe-app/tsconfig.app.json b/mfe-app/tsconfig.app.json
new file mode 100644
index 0000000..aeff964
--- /dev/null
+++ b/mfe-app/tsconfig.app.json
@@ -0,0 +1,23 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+ "target": "ES2022",
+ "useDefineForClassFields": true,
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "types": ["vite/client"],
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "baseUrl": "."
+ },
+ "include": ["src"]
+}
diff --git a/mfe-app/tsconfig.json b/mfe-app/tsconfig.json
new file mode 100644
index 0000000..f6df6c7
--- /dev/null
+++ b/mfe-app/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ]
+}
diff --git a/mfe-app/tsconfig.node.json b/mfe-app/tsconfig.node.json
new file mode 100644
index 0000000..8cdf5e8
--- /dev/null
+++ b/mfe-app/tsconfig.node.json
@@ -0,0 +1,11 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "lib": ["ES2022"],
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "noEmit": true,
+ "strict": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/mfe-app/vite.config.ts b/mfe-app/vite.config.ts
new file mode 100644
index 0000000..84a9d93
--- /dev/null
+++ b/mfe-app/vite.config.ts
@@ -0,0 +1,25 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+
+export default defineConfig({
+ plugins: [react()],
+ server: {
+ port: 4300,
+ host: 'localhost',
+ open: false,
+ cors: {
+ origin: '*',
+ credentials: false,
+ methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
+ allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
+ },
+ headers: {
+ 'Access-Control-Allow-Origin': '*',
+ 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With',
+ },
+ },
+ build: {
+ outDir: 'dist',
+ },
+});
From cccd03820ccf568e7d23ec3b1df938732f183242 Mon Sep 17 00:00:00 2001
From: AMAN SINGH <36499868+aman06it@users.noreply.github.com>
Date: Fri, 5 Jun 2026 16:08:49 +0530
Subject: [PATCH 02/10] planning doc removed
---
docs/multi-framework-recipes-ui-plan.md | 383 ------------------------
1 file changed, 383 deletions(-)
delete mode 100644 docs/multi-framework-recipes-ui-plan.md
diff --git a/docs/multi-framework-recipes-ui-plan.md b/docs/multi-framework-recipes-ui-plan.md
deleted file mode 100644
index 2abd969..0000000
--- a/docs/multi-framework-recipes-ui-plan.md
+++ /dev/null
@@ -1,383 +0,0 @@
-# Multi-Framework Recipes UI — Design Doc
-
-| | |
-|---|---|
-| **Status** | Draft — for senior review |
-| **Owner** | Aman Singh |
-| **Reviewers** | Manuel Jasso, Drew Ellison, Navateja Alagam, Theodore Lau, Charles Watkins |
-| **Last updated** | 2026-05-13 |
-| **Target milestone** | 264 release · July feature freeze |
-
----
-
-## 1. Summary
-
-Reposition the existing "React Recipes" site under [force-app/main/react-recipes/](../force-app/main/react-recipes/) as **Multi-Framework Recipes**. Add two browse axes (Hosting, Framework) as first-class IA(Information Architecture) without disrupting the pro-code Salesforce developer experience that the recipe set is built for. Push advanced server / SSL / local-host concerns to a separate, future repo and link out instead of inlining.
-
-This document specifies the **information architecture, data model, URL contract, and rollout plan**. It is the artifact for sign-off before implementation begins.
-
-### Goals
-
-- Make Hosting (`Salesforce-Hosted` vs `Externally-Hosted`) a navigable axis, not just a chip.
-- Make Framework (`React`, `Vue`, `Angular`) a switchable axis with graceful "(soon)" states for non-shipping frameworks.
-- Keep the default user journey simple: deploy and go. Audience is pro-code SF devs, not Node experts.
-- Provide a clear, opt-in pointer to the future advanced server repo.
-
-### Non-goals
-
-- Authoring Vue or Angular recipe content (the IA must accommodate them; content lands later).
-- Heroku-hosted demo server for externally-hosted scenarios — explicitly deferred per the meeting.
-- Replacing the trail/learn/docs site. This is a recipes catalog, not docs.
-
----
-
-## 2. Background
-
-Source meeting: *Discuss multi-framework & multi-domain recipes* (2026-05-06).
-
-### Decisions locked in the meeting
-
-| Decision | UI implication |
-|---|---|
-| MFE recipes are **part of** multi-framework recipes, not a separate site | One unified catalog |
-| Two hosting modes: **Salesforce-Hosted** (`*.salesforce.app`) and **Externally Hosted** (third-party) | Hosting is a first-class browse axis |
-| Multi-framework: React first, then Angular & Vue | Framework is a first-class browse axis |
-| Server repo is **separate** (advanced: local Express, SSL, `/etc/hosts`, Heroku deploy) | UI links out; never inlines |
-| Audience = pro-code SF devs, often only know SF dev | Default path stays "deploy & go" |
-| GA scope = local server only (Heroku-hosted demo deferred) | Copy must not promise hosted demos |
-
----
-
-## 3. Current state
-
-### What exists
-
-| Surface | Status | File |
-|---|---|---|
-| Recipe registry with `hosting` and `framework` tags | Present | [recipeRegistry.ts](../force-app/main/react-recipes/uiBundles/reactRecipes/src/recipeRegistry.ts) |
-| Home page with category tiles + hosting/framework chips | Present | [Home.tsx](../force-app/main/react-recipes/uiBundles/reactRecipes/src/pages/Home.tsx) |
-| Sidebar + Guest/Host-JS/Host-HTML tab template | Present | [Mfe.tsx](../force-app/main/react-recipes/uiBundles/reactRecipes/src/pages/Mfe.tsx) |
-| Global Cmd+K search across the registry | Present | [SearchBar.tsx](../force-app/main/react-recipes/uiBundles/reactRecipes/src/components/app/SearchBar.tsx) |
-| Flat per-category routes | Present | [routes.tsx](../force-app/main/react-recipes/uiBundles/reactRecipes/src/routes.tsx) |
-
-### Gaps vs. strategic direction
-
-1. Hosting chips are visible but not actionable — no filter.
-2. Title and visual identity hard-code "React".
-3. No explanation of the SF-hosted vs externally-hosted distinction; reviewers in the meeting needed it walked through verbally.
-4. No pointer to the future server / advanced-hosting repo.
-5. `Mfe.tsx` is hardcoded to `Externally-Hosted` + `React`; no mechanism for flavors of the same recipe.
-6. `recipeRegistry` has a 1:1 recipe→hosting and recipe→framework relationship; cannot represent a recipe that has both an SF-hosted and an externally-hosted flavor without duplicating entries.
-
----
-
-## 4. Design decisions
-
-Locked via review on 2026-05-13:
-
-| # | Question | Decision |
-|---|---|---|
-| D1 | Top-level IA shape | **Filters + categories.** Category tiles stay primary. Framework switcher in navbar; Hosting filter on catalog. |
-| D2 | Per-recipe flavor UX | **Tabs on the recipe page.** Disabled "(soon)" for unbuilt flavors; URL-addressable. |
-| D3 | Server-repo link prominence | **Footer + contextual callout** on Externally-Hosted recipes only. |
-| D4 | Site rebrand | **Yes, now.** "Multi-Framework Recipes" with a current-framework chip. |
-
-### Rationale (for D1)
-
-Considered three IA shapes:
-
-- **Filters + categories** *(chosen)* — minimal disruption, scales to Vue/Angular without new top-level surfaces.
-- *Two tracks (SF-Hosted / Externally Hosted)* — too much duplication; many recipes are framework-pure and hosting-agnostic.
-- *Flat catalog with multi-axis filters* — strong for power users, weak as a first-time learning surface; the audience here is learning-oriented.
-
-### Rationale (for D2)
-
-Considered three flavor UX options. Rejected:
-
-- *Global navbar switchers* — too far from the content, surprising URL changes.
-- *Side-by-side comparison* — high build cost, poor mobile fit, premature when only one flavor exists per recipe today.
-
----
-
-## 5. Data model
-
-The current registry assumes one recipe = one (hosting × framework) combination. This must change to support flavors.
-
-### Current shape — [recipeRegistry.ts](../force-app/main/react-recipes/uiBundles/reactRecipes/src/recipeRegistry.ts)
-
-```ts
-interface RecipeEntry {
- name: string;
- description: string;
- category: string;
- categoryRoute: string;
- recipeIndex: number;
- hosting: Hosting; // single value
- framework: Framework; // single value
-}
-```
-
-### Proposed shape
-
-```ts
-type Hosting = 'salesforce-hosted' | 'externally-hosted';
-type Framework = 'react' | 'vue' | 'angular';
-
-interface RecipeFlavor {
- hosting: Hosting;
- framework: Framework;
- /** Source-file imports for code tabs. Shape varies by category. */
- sources: Record;
- /** Optional flavor-specific description override. */
- notes?: string;
-}
-
-interface RecipeEntry {
- /** Stable ID, kebab-case, unique within a category. e.g. "basic-embed" */
- id: string;
- name: string;
- description: string;
- category: string;
- categoryRoute: string;
- recipeIndex: number;
- /** At least one. The first entry is the default flavor. */
- flavors: RecipeFlavor[];
-}
-```
-
-Rules:
-
-- A recipe **must** have at least one flavor.
-- Default flavor = `flavors[0]`.
-- `(hosting, framework)` is unique within a recipe's flavors.
-- Filtering "show me Salesforce-Hosted" matches a recipe if **any** of its flavors is `salesforce-hosted`.
-
-### Helper functions (registry API)
-
-```ts
-getRecipe(categoryRoute: string, id: string): RecipeEntry | undefined
-listRecipes(filter?: { hosting?: Hosting; framework?: Framework }): RecipeEntry[]
-hasFlavor(recipe: RecipeEntry, hosting: Hosting, framework: Framework): boolean
-resolveFlavor(recipe: RecipeEntry, hosting?: Hosting, framework?: Framework): RecipeFlavor
-```
-
-`resolveFlavor` fallback order: exact match → match on `hosting` only → match on `framework` only → `flavors[0]`. The UI uses the resolved flavor's actual `(hosting, framework)` to update visible state — never silently shows wrong content.
-
-### Migration
-
-Today's registry entries map cleanly: each becomes a recipe with a single-element `flavors` array. No content rewrite needed; the registry shape changes and call sites are updated.
-
----
-
-## 6. URL contract
-
-URLs are the source of truth for state. Reload must restore the exact same view.
-
-| Surface | URL | Notes |
-|---|---|---|
-| Catalog | `/` | Hosting filter persisted to `localStorage`; not in URL (filter is a browse aid, not shareable state). |
-| Category | `/{category}` | e.g. `/mfe` |
-| Recipe — default flavor | `/{category}?recipe={index}` | Backwards compatible with current behavior in [SearchBar.tsx:91-95](../force-app/main/react-recipes/uiBundles/reactRecipes/src/components/app/SearchBar.tsx#L91-L95). |
-| Recipe — explicit flavor | `/{category}?recipe={index}&host={hosting}&fw={framework}` | `host ∈ {sf, ext}`, `fw ∈ {react, vue, angular}`. Short forms keep URLs scannable. |
-| Framework switcher | Navbar control | Updates the in-page selection only; does not navigate. Persisted to `localStorage`. |
-
-Invalid combinations are silently resolved via `resolveFlavor`. The URL is corrected via `replaceState` (not `pushState`) so the user's back button is not littered.
-
----
-
-## 7. Target UI
-
-### 7.1 Global chrome
-
-```ini
-┌──────────────────────────────────────────────────────────────────────┐
-│ ⌬ Multi-Framework Recipes [React ▾] Hello Read Modify … 🔍 │
-└──────────────────────────────────────────────────────────────────────┘
-```
-
-- Title: **Multi-Framework Recipes**.
-- Framework switcher (`React ▾`); Vue/Angular shown disabled "(soon)".
-- Categories unchanged in nav. `Embedding` is a normal category — its hosting nature is conveyed via tags.
-- New footer link: *Advanced server recipes ↗* (separate repo).
-
-### 7.2 Home / catalog
-
-```ini
-┌──────────────────────────────────────────────────────────────────────┐
-│ Multi-Framework Recipes — React │
-│ Sample patterns for Salesforce-Hosted and Externally-Hosted apps. │
-│ [Get Started] [Developer Guide ↗] │
-│ │
-│ Hosting: ● All ○ Salesforce-Hosted ○ Externally Hosted │
-│ │
-│ ┌─ Hello ─────────┐ ┌─ Read Data ─────┐ ┌─ Embedding ────┐ │
-│ │ 8 recipes │ │ 8 recipes │ │ 7 recipes │ │
-│ │ [SF] [React] │ │ [SF] [React] │ │ [Ext] [React] │ │
-│ └─────────────────┘ └─────────────────┘ └────────────────┘ │
-│ … │
-│ │
-│ ─── Hosting Models ───────────────────────────────────────────────── │
-│ 📦 Salesforce-Hosted 🌐 Externally Hosted │
-│ Deploy your app to Host on AWS / Heroku / │
-│ Salesforce.app (UI bundles). your own infra; embed via lwc-shell. │
-│ Best for new apps. Best for existing apps & SaaS vendors. │
-│ │
-│ Want to run your own server locally with SSL? │
-│ → Advanced server recipes (separate repo) ↗ │
-└──────────────────────────────────────────────────────────────────────┘
-```
-
-### 7.3 Category page
-
-Two layouts based on category type:
-
-- **Standard categories** (`/hello`, `/read-data`, …): existing recipe-card list. Add small `[SF] [React]` chips on each card.
-- **Multi-source categories** (`/mfe` and future similar): sidebar + tabbed-code layout from `Mfe.tsx`. Add the flavor switcher (§7.4) above the code tabs.
-
-### 7.4 Recipe page — flavor switcher
-
-```ini
-┌─ Basic Embed ────────────────────────────────────────────────┐
-│ Minimum viable lwc-shell embed. │
-│ │
-│ Hosting: [● Externally Hosted] [ Salesforce-Hosted ⓘ ] │
-│ Framework: [● React] [ Vue (soon) ] [ Angular (soon) ] │
-│ │
-│ ⓘ Externally hosted: this recipe assumes the guest app runs │
-│ on a third-party domain. For local dev with SSL, see │
-│ → Advanced server recipes ↗ │
-│ │
-│ ┌ Code ─────────────────────────────────────────────────────┐│
-│ │ [Guest (React)] [Host (LWC JS)] [Host (LWC HTML)] ││
-│ │ … ││
-│ └───────────────────────────────────────────────────────────┘│
-└──────────────────────────────────────────────────────────────┘
-```
-
-- Both switchers always visible. Unavailable flavors disabled "(soon)" — not hidden.
-- Tooltip on disabled flavors explains *why* it's unavailable ("Vue recipe coming in 266", "SF-hosted variant in design").
-- Selection reflected in URL per §6.
-- Externally-Hosted recipes get the inline callout linking to the server repo.
-
----
-
-## 8. Accessibility
-
-| Surface | Requirement |
-|---|---|
-| Hosting filter | `role="radiogroup"` with arrow-key navigation; each option is a `role="radio"` button. Keyboard-equivalent of a click. |
-| Framework switcher | `role="combobox"` with proper `aria-expanded`, `aria-controls`, focus trapping in the menu, Escape to close. |
-| Flavor tabs | `role="tablist"` / `role="tab"`. Disabled tabs use `aria-disabled="true"` (not `disabled`) so SR users still discover the "(soon)" state. |
-| Color | Hosting/framework chips must not rely on color alone; always include a text label. |
-| Focus | Visible focus ring on all interactive elements (already enforced via `focus-visible:ring-ring` in current Tailwind setup). |
-
-The existing search bar already meets keyboard-nav expectations (arrow keys, Enter, Esc) — see [SearchBar.tsx:100-111](../force-app/main/react-recipes/uiBundles/reactRecipes/src/components/app/SearchBar.tsx#L100-L111). New components match that bar.
-
----
-
-## 9. Search
-
-The Cmd+K search ([SearchBar.tsx](../force-app/main/react-recipes/uiBundles/reactRecipes/src/components/app/SearchBar.tsx)) currently matches `name` and `description` only (lines 56–59). Extension:
-
-- Index `hosting` and `framework` keywords so "external", "vue", "angular", "salesforce-hosted" surface relevant recipes.
-- Show flavor chips on each result row so users can disambiguate before clicking.
-- Selecting a result navigates to the recipe with the matching flavor pre-selected via URL params.
-
----
-
-## 10. Use-case → UI coverage
-
-| Discussion point | UI surface |
-|---|---|
-| MFE included in multi-framework recipes | `Embedding` is a normal category tile |
-| Two hosting types: SF-hosted vs externally-hosted | Hosting filter on Home + flavor tabs on recipe page |
-| Multi-framework: React → Vue → Angular | Navbar framework switcher + per-recipe flavor chips with "(soon)" |
-| `salesforce.app` family vs 3P explanation | "Hosting Models" block on Home |
-| SDK/bridge communication | Existing Guest / Host code tabs in `Mfe.tsx` |
-| Local-server / SSL / `/etc/hosts` (advanced) | Separate repo; linked from footer + Externally-Hosted recipes |
-| GA scope = local server only | Copy says *"local Express + SSL"*; nothing promises hosted demos |
-| Pro-code SF devs new to Node | Default path stays "deploy & go"; complexity is opt-in via outlinks |
-
----
-
-## 11. Build plan
-
-Each step is a small, shippable PR. Ownership is implicit (single dev) but each step has clear acceptance criteria for review.
-
-| # | Step | Files | Acceptance |
-|---|---|---|---|
-| 1 | Registry shape change: introduce `flavors[]` | [recipeRegistry.ts](../force-app/main/react-recipes/uiBundles/reactRecipes/src/recipeRegistry.ts) | All current entries migrate to single-flavor form; existing pages render unchanged; types are strict. |
-| 2 | Rebrand to *Multi-Framework Recipes* + disabled framework switcher | [Navbar.tsx](../force-app/main/react-recipes/uiBundles/reactRecipes/src/components/app/Navbar.tsx), [Home.tsx](../force-app/main/react-recipes/uiBundles/reactRecipes/src/pages/Home.tsx), `index.html` | Page title, H1, and navbar reflect new brand; switcher renders with disabled Vue/Angular. |
-| 3 | Home Hosting filter | [Home.tsx](../force-app/main/react-recipes/uiBundles/reactRecipes/src/pages/Home.tsx) | Radio group filters tile grid; selection persists across reload; a11y per §8. |
-| 4 | Hosting Models explainer + footer link | [Home.tsx](../force-app/main/react-recipes/uiBundles/reactRecipes/src/pages/Home.tsx), new `Footer.tsx` | Two-column block + footer link; placeholder URL acceptable v1. |
-| 5 | Recipe-card chips on standard category pages | All non-MFE pages under [pages/](../force-app/main/react-recipes/uiBundles/reactRecipes/src/pages/) | Each card shows hosting + framework chip; same component as Home. |
-| 6 | Flavor switcher on `Mfe.tsx` | [Mfe.tsx](../force-app/main/react-recipes/uiBundles/reactRecipes/src/pages/Mfe.tsx) | URL-driven per §6; disabled "(soon)" states; `resolveFlavor` fallback. |
-| 7 | Contextual server-repo callout on Externally-Hosted recipes | `Mfe.tsx` + future externally-hosted pages | Shown only when current flavor is externally-hosted. |
-| 8 | Search index hosting + framework keywords | [SearchBar.tsx](../force-app/main/react-recipes/uiBundles/reactRecipes/src/components/app/SearchBar.tsx) | "external", "vue" surface relevant recipes; result rows show flavor chips. |
-
-Steps 1 and 2 are sequential (everything else builds on them). 3–8 are largely independent.
-
-### Out of scope (deferred)
-
-- Side-by-side comparison view of flavors.
-- Heroku-hosted demo server.
-- Vue and Angular *content* — IA scaffolding only.
-- Mobile-specific layout work (the recipes site is desktop-first; nothing here regresses mobile but no enhancements either).
-
----
-
-## 12. Testing
-
-| Layer | Approach |
-|---|---|
-| Registry shape | Type-level: strict TS; runtime: assertion in registry init that every recipe has ≥1 flavor and `(hosting, framework)` pairs are unique within a recipe. Throws at module load if violated — fast failure beats a misleading UI. |
-| Filters | Component test: Hosting filter narrows the tile list to the correct count; "All" restores. |
-| Flavor switcher | Component test: clicking each flavor updates URL; reload restores selection; invalid URL params resolve to default without 500. |
-| Search | Test that hosting/framework keywords return expected recipes. |
-| Visual | Manual walkthrough of every category page after step 5; smoke test in the dev server before each PR is merged. |
-
-The repo uses Jest already ([jest.config.js](../jest.config.js)). New tests follow the same pattern; no new test infra.
-
----
-
-## 13. Rollout
-
-- All steps are merged to `main` behind no flag — the recipes site is a static React app; there's no runtime risk to existing Salesforce orgs.
-- Deployment is a normal SFDX push of the React UI bundle.
-- "(soon)" labels are content-controlled in the registry; flipping a flavor from disabled to enabled is one PR, no infra change.
-- Server repo URL is configurable in one place ([Footer.tsx](../force-app/main/react-recipes/uiBundles/reactRecipes/src/components/app/Footer.tsx) constant) so it can flip from placeholder to real without touching every callsite.
-
----
-
-## 14. Risks
-
-| Risk | Likelihood | Impact | Mitigation |
-|---|---|---|---|
-| 264 freeze (July) — tight window for repo creation + content authoring | Med | High | Ship UI scaffolding (steps 1–8) ahead of MFE recipe content; backfill content when ready. |
-| Server repo not yet created when we ship link | High | Low | Ship with placeholder URL + "Coming soon" tooltip. Single constant to flip. |
-| "(soon)" labels age badly if Vue/Angular slip | Med | Low | Generic copy ("Additional frameworks planned") if a target slips beyond a release. |
-| Registry refactor breaks existing pages | Low | Med | Migration is mechanical (single flavor wrapping); covered by step 1's acceptance criteria. |
-| SF-hosted Embedding flavor design not finalized when step 6 lands | Med | Low | Step 6 only requires the *mechanism*; SF-hosted flavor stays disabled until content exists. |
-
----
-
-## 15. Open questions
-
-These need answers from §4 reviewers before step 2 ships:
-
-1. **Server-repo name and URL** — confirmed name from the open-source team? Charles Watkins is the contact per the meeting notes.
-2. **Framework switcher persistence** — proposed: `localStorage` per-user, default = `react`. Accept?
-3. **Future SF-hosted Embedding flavors** — confirmed: same recipe with multiple `flavors[]` (per §5). Reviewer should confirm this matches the team's mental model before we commit to the registry refactor.
-4. **Disabled-flavor copy** — generic "(soon)" or release-specific ("266")? Recommend release-specific where known, generic otherwise.
-
----
-
-## 16. Appendix — alternatives considered and rejected
-
-| Alternative | Why rejected |
-|---|---|
-| Two top-level tracks (SF-Hosted / Externally Hosted) | Most recipes are framework-pure; track-splitting would duplicate the catalog and force users to switch tracks for unrelated content. |
-| Flat catalog with multi-axis filters as the primary surface | Strong for power users, weak as a first-time learning surface. Audience is learning-oriented. |
-| Inline the server / SSL / `/etc/hosts` content into the main repo | Directly contradicts the meeting's "separation of concerns" decision and the "simple deploy & go" audience principle. |
-| Side-by-side flavor comparison on recipe pages | Premature when only one flavor exists per recipe today; high build cost; poor mobile fit. |
-| Leave site title as "React Recipes" until Vue/Angular ship | Contradicts the strategic framing of the meeting; the UI should set expectations now. |
From b0dd644941dda7fbe6e352965d153cb22bcfa5c7 Mon Sep 17 00:00:00 2001
From: AMAN SINGH
Date: Thu, 18 Jun 2026 12:43:44 +0530
Subject: [PATCH 03/10] feat(embedding): migrate to Platform SDK +
Replaces the legacy @salesforce/experimental-mfe-bridge / vendored
stack with the GA-track @salesforce/platform-sdk and the
base component, matching the lo2demo-sfdx
react-sdk pattern.
Guest (mfe-app):
- Switch dependency from @salesforce/experimental-mfe-bridge to
@salesforce/platform-sdk@10.9.2; add side-effect import
@salesforce/platform-sdk/sf-embedding in main.tsx
- Add SdkProvider context + useSdk() hook; resolve chat/view SDKs
once at startup via top-level await Promise.all
- Rewrite all six recipes against the SDK surface:
- BasicEmbed: chat.getHostContext()
- ReceiveData: view.getUiProps() + URLSearchParams fallback
- SendEvent: view.dispatchEvent(name, data)
- AutoResize: view.resize() driven by ResizeObserver
- ThemeTokens: view.getTheme() + chat.getHostContext()
- DirtyState: view.markDirtyState() / clearDirtyState()
- Drop GraphQLBridge recipe (host-side bridge.graphql() retired)
Host (LWC):
- Replace each mfe* host with a declarative
wrapper; drop imperative document.createElement('lwc-shell') and
c/vendorLwcShell imports
- Delete vendored vendorLwcShell (no longer needed;
is provided by the platform)
- Delete mfeGraphQL host
Explorer / docs:
- Rename Embedding route /mfe -> /embedding
- Update Mfe.tsx, recipeRegistry, READMEs, CSP description, and Home
page copy to reflect the SDK-shaped recipes
---
README.md | 23 +-
.../MfeAppLocalhost.cspTrustedSite-meta.xml | 2 +-
.../lwc/mfeAutoResize/mfeAutoResize.html | 16 +-
.../lwc/mfeAutoResize/mfeAutoResize.js | 36 +-
.../lwc/mfeBasicEmbed/mfeBasicEmbed.html | 13 +-
.../lwc/mfeBasicEmbed/mfeBasicEmbed.js | 31 +-
.../lwc/mfeDirtyState/mfeDirtyState.html | 20 +-
.../lwc/mfeDirtyState/mfeDirtyState.js | 93 +-
.../default/lwc/mfeGraphQL/mfeGraphQL.html | 12 -
.../main/default/lwc/mfeGraphQL/mfeGraphQL.js | 50 -
.../lwc/mfeGraphQL/mfeGraphQL.js-meta.xml | 10 -
.../lwc/mfeReceiveData/mfeReceiveData.html | 16 +-
.../lwc/mfeReceiveData/mfeReceiveData.js | 52 +-
.../lwc/mfeSendEvent/mfeSendEvent.html | 21 +-
.../default/lwc/mfeSendEvent/mfeSendEvent.js | 42 +-
.../lwc/mfeThemeTokens/mfeThemeTokens.html | 20 +-
.../lwc/mfeThemeTokens/mfeThemeTokens.js | 41 +-
.../lwc/vendorLwcShell/vendorLwcShell.js | 766 -----
.../vendorLwcShell/vendorLwcShell.js-meta.xml | 5 -
.../src/components/app/Navbar.tsx | 2 +-
.../uiBundles/reactRecipes/src/pages/Home.tsx | 6 +-
.../uiBundles/reactRecipes/src/pages/Mfe.tsx | 36 +-
.../reactRecipes/src/recipeRegistry.ts | 34 +-
.../reactRecipes/src/recipes/README.md | 17 +-
.../uiBundles/reactRecipes/src/routes.tsx | 4 +-
mfe-app/package-lock.json | 2607 +++++++++++++++++
mfe-app/package.json | 2 +-
mfe-app/src/App.tsx | 3 -
mfe-app/src/main.tsx | 14 +-
mfe-app/src/recipes/AutoResize.tsx | 40 +-
mfe-app/src/recipes/BasicEmbed.tsx | 50 +-
mfe-app/src/recipes/DirtyState.tsx | 36 +-
mfe-app/src/recipes/GraphQLBridge.tsx | 124 -
mfe-app/src/recipes/ReceiveData.tsx | 71 +-
mfe-app/src/recipes/SendEvent.tsx | 30 +-
mfe-app/src/recipes/ThemeTokens.tsx | 91 +-
mfe-app/src/sdk-context.tsx | 19 +
37 files changed, 2942 insertions(+), 1513 deletions(-)
delete mode 100644 force-app/main/default/lwc/mfeGraphQL/mfeGraphQL.html
delete mode 100644 force-app/main/default/lwc/mfeGraphQL/mfeGraphQL.js
delete mode 100644 force-app/main/default/lwc/mfeGraphQL/mfeGraphQL.js-meta.xml
delete mode 100644 force-app/main/default/lwc/vendorLwcShell/vendorLwcShell.js
delete mode 100644 force-app/main/default/lwc/vendorLwcShell/vendorLwcShell.js-meta.xml
create mode 100644 mfe-app/package-lock.json
delete mode 100644 mfe-app/src/recipes/GraphQLBridge.tsx
create mode 100644 mfe-app/src/sdk-context.tsx
diff --git a/README.md b/README.md
index 2c0f126..b5f98fd 100644
--- a/README.md
+++ b/README.md
@@ -40,16 +40,14 @@ Today this mode lives in [`force-app/main/react-recipes/uiBundles/reactRecipes/`
### Externally Hosted
-The framework app runs on your own infrastructure (Vercel, AWS, anywhere) and is embedded into a Salesforce Lightning page via `lwc-shell`. A postMessage bridge proxies data, events, and GraphQL between the two sides.
+The framework app runs on your own infrastructure (Vercel, AWS, anywhere) and is embedded into a Salesforce Lightning page via the standard `` base component. The Platform SDK is the contract on the guest side; the sf-embedding protocol is the wire format underneath.
```mermaid
graph LR
- A[External Framework App mfe-app/] -->|iframe src| B[lwc-shell]
+ A[External Framework App mfe-app/] -->|iframe src| B[lightning-embedding]
B -->|embedded in| C[LWC Host Component]
C -->|deployed to| D[Salesforce Org]
- B <-->|postMessage bridge| A
- D -->|updateData / events| B
- A -->|bridge.graphql| D
+ A <-->|"@salesforce/platform-sdk"| B
```
**Use when:** you already have an externally hosted app, need your own build/release cadence, or want to reuse the same app across Salesforce and non-Salesforce surfaces.
@@ -208,7 +206,7 @@ These recipes run as a Salesforce UI Bundle served directly from the org. Today
## Install & Run Externally Hosted Recipes
-These recipes run an external framework app on your own server and embed it into Salesforce via `lwc-shell`. In development, "externally hosted" means `localhost:4300`; in production you would point the LWC host at your deployed URL. Today the only implementation is React; Vue and Angular are planned.
+These recipes run an external framework app on your own server and embed it into Salesforce via the standard `` base component. In development, "externally hosted" means `localhost:4300`; in production you would point the LWC host at your deployed URL. Today the only implementation is React; Vue and Angular are planned.
> **Note:** The CSP trusted site for `localhost:4300` is included in the shared metadata deployed in the org setup steps above. No additional metadata deploy is needed.
@@ -240,13 +238,12 @@ These recipes run an external framework app on your own server and embed it into
| LWC host component | External app route | What it demonstrates |
|---|---|---|
-| `mfeBasicEmbed` | `/basic-embed` | Minimum viable embed — bridge connection detection |
-| `mfeReceiveData` | `/receive-data` | Host pushes data into guest via `shell.updateData()` |
-| `mfeSendEvent` | `/send-event` | Guest dispatches events to host via `bridge.dispatchEvent()` |
-| `mfeAutoResize` | `/auto-resize` | iframe height follows guest content via ResizeObserver |
-| `mfeThemeTokens` | `/theme-tokens` | Salesforce CSS custom properties synced to guest |
-| `mfeDirtyState` | `/dirty-state` | Guest notifies host of unsaved changes |
-| `mfeGraphQL` | `/graphql-bridge` | Guest queries Salesforce GraphQL proxied through host |
+| `mfeBasicEmbed` | `/basic-embed` | Minimum viable embed using `` — `chatSDK.getHostContext()` for connection detection |
+| `mfeReceiveData` | `/receive-data` | Host re-mounts `` with new src carrying URL query params; guest reads via `URLSearchParams` and `viewSDK.getUiProps()` |
+| `mfeSendEvent` | `/send-event` | Guest calls `viewSDK.dispatchEvent(name, data)`; surface-level routing is host-runtime specific |
+| `mfeAutoResize` | `/auto-resize` | Guest tracks body height with ResizeObserver and calls `viewSDK.resize()` |
+| `mfeThemeTokens` | `/theme-tokens` | Guest reads host theme via `viewSDK.getTheme()` and the broader environment via `chatSDK.getHostContext()` |
+| `mfeDirtyState` | `/dirty-state` | Guest calls `viewSDK.markDirtyState()` / `clearDirtyState()` to signal unsaved changes |
### Pointing at a deployed external app
diff --git a/force-app/main/default/cspTrustedSites/MfeAppLocalhost.cspTrustedSite-meta.xml b/force-app/main/default/cspTrustedSites/MfeAppLocalhost.cspTrustedSite-meta.xml
index 0987ccd..1003d18 100644
--- a/force-app/main/default/cspTrustedSites/MfeAppLocalhost.cspTrustedSite-meta.xml
+++ b/force-app/main/default/cspTrustedSites/MfeAppLocalhost.cspTrustedSite-meta.xml
@@ -3,7 +3,7 @@
false
false
All
- Local MFE app dev server — allows the lwc-shell iframe to load from localhost:4300
+ Local MFE app dev server — allows the lightning-embedding iframe to load from localhost:4300
http://localhost:4300
true
false
diff --git a/force-app/main/default/lwc/mfeAutoResize/mfeAutoResize.html b/force-app/main/default/lwc/mfeAutoResize/mfeAutoResize.html
index b5e5c03..9a874a7 100644
--- a/force-app/main/default/lwc/mfeAutoResize/mfeAutoResize.html
+++ b/force-app/main/default/lwc/mfeAutoResize/mfeAutoResize.html
@@ -2,11 +2,19 @@
- Add or remove items in the MFE — the iframe height adjusts automatically.
- No fixed height is set. The bridge reports content height changes via a
- ResizeObserver.
+ Add or remove items in the MFE — the guest calls
+ viewSDK.resize() on every body-size change. The
+ host applies the requested dimensions to the embedding
+ container.
-
+
+
+
diff --git a/force-app/main/default/lwc/mfeAutoResize/mfeAutoResize.js b/force-app/main/default/lwc/mfeAutoResize/mfeAutoResize.js
index 58b1400..bd19d16 100644
--- a/force-app/main/default/lwc/mfeAutoResize/mfeAutoResize.js
+++ b/force-app/main/default/lwc/mfeAutoResize/mfeAutoResize.js
@@ -1,46 +1,12 @@
import { LightningElement, api } from 'lwc';
-import 'c/vendorLwcShell';
export default class MfeAutoResize extends LightningElement {
@api baseUrl = 'http://localhost:4300';
- _shellElement;
+ debug = false;
get computedSrc() {
const url = new URL(this.baseUrl);
url.pathname = '/auto-resize';
return url.toString();
}
-
- renderedCallback() {
- this._initializeShell();
- }
-
- disconnectedCallback() {
- this._shellElement = null;
- }
-
- _initializeShell() {
- if (this._shellElement) {
- return;
- }
- const container = this.template.querySelector('.shell-container');
- if (!container) {
- return;
- }
- const shell = document.createElement('lwc-shell');
- shell.sandbox = 'allow-forms allow-modals';
- shell.title = 'MFE Auto-Resize';
- shell.src = this.computedSrc;
-
- // No explicit resize handler needed — lwc-shell receives 'resize' events
- // from the bridge and updates iframe.style.height automatically.
- // Cancel the event here if you need a fixed height instead.
- shell.addEventListener('widget-ready', () => {
- // eslint-disable-next-line no-console
- console.log('[MfeAutoResize] widget-ready');
- });
-
- container.appendChild(shell);
- this._shellElement = shell;
- }
}
diff --git a/force-app/main/default/lwc/mfeBasicEmbed/mfeBasicEmbed.html b/force-app/main/default/lwc/mfeBasicEmbed/mfeBasicEmbed.html
index 89cc3b6..a67cb06 100644
--- a/force-app/main/default/lwc/mfeBasicEmbed/mfeBasicEmbed.html
+++ b/force-app/main/default/lwc/mfeBasicEmbed/mfeBasicEmbed.html
@@ -2,10 +2,17 @@
- Minimum viable embed. Creates an lwc-shell pointing at the
- external MFE app and handles the widget-ready event.
+ Minimum viable embed using the standard
+ <lightning-embedding> base component.
-
+
+
+
diff --git a/force-app/main/default/lwc/mfeBasicEmbed/mfeBasicEmbed.js b/force-app/main/default/lwc/mfeBasicEmbed/mfeBasicEmbed.js
index 62652a4..0202bf7 100644
--- a/force-app/main/default/lwc/mfeBasicEmbed/mfeBasicEmbed.js
+++ b/force-app/main/default/lwc/mfeBasicEmbed/mfeBasicEmbed.js
@@ -1,41 +1,12 @@
import { LightningElement, api } from 'lwc';
-import 'c/vendorLwcShell';
export default class MfeBasicEmbed extends LightningElement {
@api baseUrl = 'http://localhost:4300';
- _shellElement;
+ debug = false;
get computedSrc() {
const url = new URL(this.baseUrl);
url.pathname = '/basic-embed';
return url.toString();
}
-
- renderedCallback() {
- this._initializeShell();
- }
-
- disconnectedCallback() {
- this._shellElement = null;
- }
-
- _initializeShell() {
- if (this._shellElement) {
- return;
- }
- const container = this.template.querySelector('.shell-container');
- if (!container) {
- return;
- }
- const shell = document.createElement('lwc-shell');
- shell.sandbox = 'allow-forms allow-modals';
- shell.title = 'MFE Basic Embed';
- shell.src = this.computedSrc;
- shell.addEventListener('widget-ready', () => {
- // eslint-disable-next-line no-console
- console.log('[MfeBasicEmbed] widget-ready');
- });
- container.appendChild(shell);
- this._shellElement = shell;
- }
}
diff --git a/force-app/main/default/lwc/mfeDirtyState/mfeDirtyState.html b/force-app/main/default/lwc/mfeDirtyState/mfeDirtyState.html
index 8b3353c..87baae4 100644
--- a/force-app/main/default/lwc/mfeDirtyState/mfeDirtyState.html
+++ b/force-app/main/default/lwc/mfeDirtyState/mfeDirtyState.html
@@ -2,16 +2,20 @@
- Type into any field in the MFE form. This component receives a
- trackdirtystate event, re-dispatches it to Lightning
- Experience (which blocks tab navigation), and attaches a
- beforeunload handler so the browser also warns on
- close or reload.
+ Edit the MFE form — the guest calls
+ viewSDK.markDirtyState() on every change and
+ viewSDK.clearDirtyState() after save. The host
+ surface decides how to honour the dirty signal (e.g. block
+ navigation).
-
-
⚠ {dirtyLabel} — save or discard before leaving.
+
+
-
diff --git a/force-app/main/default/lwc/mfeDirtyState/mfeDirtyState.js b/force-app/main/default/lwc/mfeDirtyState/mfeDirtyState.js
index 1cdbb2e..0efd4c6 100644
--- a/force-app/main/default/lwc/mfeDirtyState/mfeDirtyState.js
+++ b/force-app/main/default/lwc/mfeDirtyState/mfeDirtyState.js
@@ -1,101 +1,12 @@
-import { LightningElement, api, track } from 'lwc';
-import 'c/vendorLwcShell';
+import { LightningElement, api } from 'lwc';
export default class MfeDirtyState extends LightningElement {
@api baseUrl = 'http://localhost:4300';
- @track isDirty = false;
- @track dirtyLabel = '';
- _shellElement;
- _beforeUnloadHandler;
+ debug = false;
get computedSrc() {
const url = new URL(this.baseUrl);
url.pathname = '/dirty-state';
return url.toString();
}
-
- get warningClass() {
- return this.isDirty
- ? 'slds-notify slds-notify_alert slds-theme_warning slds-m-bottom_small'
- : 'slds-hide';
- }
-
- renderedCallback() {
- this._initializeShell();
- }
-
- disconnectedCallback() {
- this._shellElement = null;
- this._detachBeforeUnload();
- }
-
- _initializeShell() {
- if (this._shellElement) {
- return;
- }
- const container = this.template.querySelector('.shell-container');
- if (!container) {
- return;
- }
- const shell = document.createElement('lwc-shell');
- shell.sandbox = 'allow-forms allow-modals';
- shell.title = 'MFE Dirty State';
- shell.src = this.computedSrc;
-
- // trackdirtystate fires when the MFE calls
- // bridge.dispatchEvent(new CustomEvent('trackdirtystate', { detail: { isDirty, label } }))
- shell.addEventListener('trackdirtystate', (evt) => {
- const isDirty = !!evt.detail.isDirty;
- const label = evt.detail.label ?? 'Unsaved changes';
- this.isDirty = isDirty;
- this.dirtyLabel = label;
-
- // Re-dispatch from the LWC itself so Lightning Experience picks it
- // up and blocks navigation with its standard unsaved-changes dialog.
- this.dispatchEvent(
- new CustomEvent('trackdirtystate', {
- detail: { isDirty, label },
- bubbles: true,
- composed: true,
- })
- );
-
- // Belt-and-suspenders: also warn on browser-level navigation
- // (tab close, reload, URL change). Works when the component is
- // hosted outside a Lightning context too.
- if (isDirty) {
- this._attachBeforeUnload();
- } else {
- this._detachBeforeUnload();
- }
- });
-
- shell.addEventListener('widget-ready', () => {
- // eslint-disable-next-line no-console
- console.log('[MfeDirtyState] widget-ready');
- });
-
- container.appendChild(shell);
- this._shellElement = shell;
- }
-
- _attachBeforeUnload() {
- if (this._beforeUnloadHandler) {
- return;
- }
- this._beforeUnloadHandler = (evt) => {
- evt.preventDefault();
- // Modern browsers ignore the custom string but require returnValue set.
- evt.returnValue = '';
- };
- window.addEventListener('beforeunload', this._beforeUnloadHandler);
- }
-
- _detachBeforeUnload() {
- if (!this._beforeUnloadHandler) {
- return;
- }
- window.removeEventListener('beforeunload', this._beforeUnloadHandler);
- this._beforeUnloadHandler = null;
- }
}
diff --git a/force-app/main/default/lwc/mfeGraphQL/mfeGraphQL.html b/force-app/main/default/lwc/mfeGraphQL/mfeGraphQL.html
deleted file mode 100644
index 68cfcaa..0000000
--- a/force-app/main/default/lwc/mfeGraphQL/mfeGraphQL.html
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
- The MFE executes Salesforce GraphQL queries via bridge.graphql().
- The host proxies requests — no allow-same-origin needed.
- Up to 10 concurrent requests are supported.
-
-
-
-
-
diff --git a/force-app/main/default/lwc/mfeGraphQL/mfeGraphQL.js b/force-app/main/default/lwc/mfeGraphQL/mfeGraphQL.js
deleted file mode 100644
index 1a26acb..0000000
--- a/force-app/main/default/lwc/mfeGraphQL/mfeGraphQL.js
+++ /dev/null
@@ -1,50 +0,0 @@
-import { LightningElement, api } from 'lwc';
-import 'c/vendorLwcShell';
-
-export default class MfeGraphQL extends LightningElement {
- @api baseUrl = 'http://localhost:4300';
- _shellElement;
-
- get computedSrc() {
- const url = new URL(this.baseUrl);
- url.pathname = '/graphql-bridge';
- return url.toString();
- }
-
- renderedCallback() {
- this._initializeShell();
- }
-
- disconnectedCallback() {
- this._shellElement = null;
- }
-
- _initializeShell() {
- if (this._shellElement) {
- return;
- }
- const container = this.template.querySelector('.shell-container');
- if (!container) {
- return;
- }
- const shell = document.createElement('lwc-shell');
- // allow-same-origin intentionally omitted — the GraphQL bridge proxies
- // queries through the host so the MFE never needs direct same-origin access.
- shell.sandbox = 'allow-forms allow-modals';
- shell.title = 'MFE GraphQL Bridge';
- shell.src = this.computedSrc;
-
- shell.addEventListener('widget-bridge-error', (evt) => {
- // eslint-disable-next-line no-console
- console.error('[MfeGraphQL] bridge error', evt.detail);
- });
-
- shell.addEventListener('widget-ready', () => {
- // eslint-disable-next-line no-console
- console.log('[MfeGraphQL] widget-ready — GraphQL bridge active');
- });
-
- container.appendChild(shell);
- this._shellElement = shell;
- }
-}
diff --git a/force-app/main/default/lwc/mfeGraphQL/mfeGraphQL.js-meta.xml b/force-app/main/default/lwc/mfeGraphQL/mfeGraphQL.js-meta.xml
deleted file mode 100644
index 620831e..0000000
--- a/force-app/main/default/lwc/mfeGraphQL/mfeGraphQL.js-meta.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
- 66.0
- true
-
- lightning__AppPage
- lightning__RecordPage
- lightning__HomePage
-
-
diff --git a/force-app/main/default/lwc/mfeReceiveData/mfeReceiveData.html b/force-app/main/default/lwc/mfeReceiveData/mfeReceiveData.html
index 602042f..d1ea53e 100644
--- a/force-app/main/default/lwc/mfeReceiveData/mfeReceiveData.html
+++ b/force-app/main/default/lwc/mfeReceiveData/mfeReceiveData.html
@@ -2,8 +2,9 @@
- Type a message and click Send — the MFE receives it via
- bridge.addEventListener('data', handler).
+ Type a message and click Send — the host re-mounts the embedding
+ with a new src carrying the value as a query
+ parameter. The MFE reads it via URLSearchParams.
diff --git a/force-app/main/default/lwc/mfeReceiveData/mfeReceiveData.js b/force-app/main/default/lwc/mfeReceiveData/mfeReceiveData.js
index 7b144b3..b20e074 100644
--- a/force-app/main/default/lwc/mfeReceiveData/mfeReceiveData.js
+++ b/force-app/main/default/lwc/mfeReceiveData/mfeReceiveData.js
@@ -1,54 +1,28 @@
import { LightningElement, api, track } from 'lwc';
-import 'c/vendorLwcShell';
export default class MfeReceiveData extends LightningElement {
@api baseUrl = 'http://localhost:4300';
@track inputValue = 'Hello from Salesforce';
- _shellElement;
-
- get computedSrc() {
- const url = new URL(this.baseUrl);
- url.pathname = '/receive-data';
- return url.toString();
- }
-
- renderedCallback() {
- this._initializeShell();
- }
-
- disconnectedCallback() {
- this._shellElement = null;
- }
+ @track committedSrc;
+ debug = false;
handleInputChange(evt) {
this.inputValue = evt.target.value;
}
handleSend() {
- if (!this._shellElement) {
- return;
- }
- // updateData() sends a postMessage into the MFE iframe.
- // The MFE receives it via bridge.addEventListener('data', handler).
- this._shellElement.updateData({ message: this.inputValue, timestamp: Date.now() });
- }
+ //
's `src` is session-binding (read at mount; cannot
+ // be mutated). Re-render the element with a new src that carries the
+ // value as a query parameter; the MFE reads it via URLSearchParams.
+ const url = new URL(this.baseUrl);
+ url.pathname = '/receive-data';
+ url.searchParams.set('message', this.inputValue);
+ url.searchParams.set('timestamp', String(Date.now()));
- _initializeShell() {
- if (this._shellElement) {
- return;
- }
- const container = this.template.querySelector('.shell-container');
- if (!container) {
- return;
- }
- const shell = document.createElement('lwc-shell');
- shell.sandbox = 'allow-forms allow-modals';
- shell.title = 'MFE Receive Data';
- shell.src = this.computedSrc;
- shell.addEventListener('widget-ready', () => {
- this._shellElement.updateData({ message: this.inputValue, timestamp: Date.now() });
+ // Force remount: clear, then set on the next microtask.
+ this.committedSrc = undefined;
+ Promise.resolve().then(() => {
+ this.committedSrc = url.toString();
});
- container.appendChild(shell);
- this._shellElement = shell;
}
}
diff --git a/force-app/main/default/lwc/mfeSendEvent/mfeSendEvent.html b/force-app/main/default/lwc/mfeSendEvent/mfeSendEvent.html
index 4bd4072..211802a 100644
--- a/force-app/main/default/lwc/mfeSendEvent/mfeSendEvent.html
+++ b/force-app/main/default/lwc/mfeSendEvent/mfeSendEvent.html
@@ -2,15 +2,20 @@
- Click an action button in the MFE — the event arrives here via
- shell.addEventListener('mfe-action', handler).
+ Click an action button in the MFE — the guest calls
+ viewSDK.dispatchEvent(name, data). Surface-level
+ routing of those events to LWC code is host-runtime specific;
+ check your surface (MCPApps, OpenAI, etc.) for the listener
+ contract.
-
-
- Last action from MFE: {lastAction}
-
-
-
+
+
+
diff --git a/force-app/main/default/lwc/mfeSendEvent/mfeSendEvent.js b/force-app/main/default/lwc/mfeSendEvent/mfeSendEvent.js
index f52fb8f..b5515e4 100644
--- a/force-app/main/default/lwc/mfeSendEvent/mfeSendEvent.js
+++ b/force-app/main/default/lwc/mfeSendEvent/mfeSendEvent.js
@@ -1,50 +1,12 @@
-import { LightningElement, api, track } from 'lwc';
-import 'c/vendorLwcShell';
+import { LightningElement, api } from 'lwc';
export default class MfeSendEvent extends LightningElement {
@api baseUrl = 'http://localhost:4300';
- @track lastAction = '';
- _shellElement;
+ debug = false;
get computedSrc() {
const url = new URL(this.baseUrl);
url.pathname = '/send-event';
return url.toString();
}
-
- renderedCallback() {
- this._initializeShell();
- }
-
- disconnectedCallback() {
- this._shellElement = null;
- }
-
- _initializeShell() {
- if (this._shellElement) {
- return;
- }
- const container = this.template.querySelector('.shell-container');
- if (!container) {
- return;
- }
- const shell = document.createElement('lwc-shell');
- shell.sandbox = 'allow-forms allow-modals';
- shell.title = 'MFE Send Event';
- shell.src = this.computedSrc;
-
- // MFE dispatches bridge.dispatchEvent(new CustomEvent('mfe-action', { detail }))
- // lwc-shell re-dispatches it here as a DOM event on the shell element.
- shell.addEventListener('mfe-action', (evt) => {
- this.lastAction = `${evt.detail.action} at ${new Date(evt.detail.timestamp).toLocaleTimeString()}`;
- });
-
- shell.addEventListener('widget-ready', () => {
- // eslint-disable-next-line no-console
- console.log('[MfeSendEvent] widget-ready');
- });
-
- container.appendChild(shell);
- this._shellElement = shell;
- }
}
diff --git a/force-app/main/default/lwc/mfeThemeTokens/mfeThemeTokens.html b/force-app/main/default/lwc/mfeThemeTokens/mfeThemeTokens.html
index 674fa4e..72a3801 100644
--- a/force-app/main/default/lwc/mfeThemeTokens/mfeThemeTokens.html
+++ b/force-app/main/default/lwc/mfeThemeTokens/mfeThemeTokens.html
@@ -2,15 +2,19 @@
- Salesforce CSS custom properties are sent to the MFE automatically on connect.
- Click Refresh to re-sync after a theme change.
+ The host's theme mode is delivered to the MFE through the
+ Platform SDK. The guest reads it via
+ viewSDK.getTheme() and the broader environment
+ via chatSDK.getHostContext().
-
-
+
+
+
diff --git a/force-app/main/default/lwc/mfeThemeTokens/mfeThemeTokens.js b/force-app/main/default/lwc/mfeThemeTokens/mfeThemeTokens.js
index 04fadfb..24568bb 100644
--- a/force-app/main/default/lwc/mfeThemeTokens/mfeThemeTokens.js
+++ b/force-app/main/default/lwc/mfeThemeTokens/mfeThemeTokens.js
@@ -1,51 +1,12 @@
import { LightningElement, api } from 'lwc';
-import 'c/vendorLwcShell';
export default class MfeThemeTokens extends LightningElement {
@api baseUrl = 'http://localhost:4300';
- _shellElement;
+ debug = false;
get computedSrc() {
const url = new URL(this.baseUrl);
url.pathname = '/theme-tokens';
return url.toString();
}
-
- renderedCallback() {
- this._initializeShell();
- }
-
- disconnectedCallback() {
- this._shellElement = null;
- }
-
- handleRefreshTheme() {
- if (!this._shellElement) {
- return;
- }
- // Re-collects all CSS custom properties from the lwc-shell element
- // and pushes them to the MFE. Call this after the host page theme changes.
- this._shellElement.refreshTheme();
- }
-
- _initializeShell() {
- if (this._shellElement) {
- return;
- }
- const container = this.template.querySelector('.shell-container');
- if (!container) {
- return;
- }
- const shell = document.createElement('lwc-shell');
- shell.sandbox = 'allow-forms allow-modals';
- shell.title = 'MFE Theme Tokens';
- shell.src = this.computedSrc;
- // Theme is sent automatically on connect. refreshTheme() re-syncs on demand.
- shell.addEventListener('widget-ready', () => {
- // eslint-disable-next-line no-console
- console.log('[MfeThemeTokens] widget-ready — theme sent automatically');
- });
- container.appendChild(shell);
- this._shellElement = shell;
- }
}
diff --git a/force-app/main/default/lwc/vendorLwcShell/vendorLwcShell.js b/force-app/main/default/lwc/vendorLwcShell/vendorLwcShell.js
deleted file mode 100644
index ad3fb3a..0000000
--- a/force-app/main/default/lwc/vendorLwcShell/vendorLwcShell.js
+++ /dev/null
@@ -1,766 +0,0 @@
-/* eslint-disable */
-// Vendored from @salesforce/experimental-mfe-lwc-shell npm package
-/*! @salesforce/experimental-mfe-lwc-shell v2.2.1-rc.1 (2026-04-09) */
-import { gql, unstable_graphql_imperative } from 'lightning/graphql';
-
-/**
- * EmbeddingResizer - Handles dynamic iframe/container resizing
- * Uses ResizeObserver to monitor element size changes and notify the host
- */
-/**
- * Generates a pseudo-random alphanumeric identifier string for unique
- * element IDs or temporary identifiers (not cryptographically secure).
- *
- * @returns Random alphanumeric string
- *
- * @example
- * getUUID(); // 'k2j8f5l9m'
- */
-function getUUID() {
- return Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString(36);
-}
-
-async function executeGraphQL(query, variables) {
- if (typeof query !== "string" || query.trim() === "") {
- throw new Error("Invalid GraphQL query: query must be a non-empty string.");
- }
- const documentNode = gql `
- ${query}
- `;
- const result = await unstable_graphql_imperative({ query: documentNode, variables });
- return { data: result?.data, errors: result?.errors };
-}
-class GraphQLBridgeHandler {
- #sendResponse;
- #pendingCount = 0;
- #queue = [];
- #maxConcurrent;
- #maxQueueSize;
- #generation = 0;
- constructor(sendResponse, maxConcurrent = 10, maxQueueSize = 50) {
- this.#sendResponse = sendResponse;
- this.#maxConcurrent = maxConcurrent;
- this.#maxQueueSize = maxQueueSize;
- }
- static isValidRequest(data) {
- const req = data;
- return (typeof req?.requestId === "string" &&
- typeof req?.query === "string" &&
- (req.variables == null || (typeof req.variables === "object" && !Array.isArray(req.variables))));
- }
- rejectInvalidRequest(data) {
- const req = data;
- if (typeof req?.requestId !== "string") {
- this.#sendResponse({
- requestId: req?.requestId,
- ok: false,
- error: { message: "Invalid GraphQL request data" },
- });
- return;
- }
- const { requestId, query, variables } = req;
- if (typeof query !== "string") {
- this.#sendResponse({
- requestId: requestId,
- ok: false,
- error: { message: "Invalid GraphQL query: query must be a string" },
- });
- return;
- }
- if (variables != null && (typeof variables !== "object" || Array.isArray(variables))) {
- this.#sendResponse({
- requestId: requestId,
- ok: false,
- error: { message: "Invalid GraphQL variables: must be a plain object" },
- });
- }
- }
- handleRequest({ requestId, query, variables }) {
- if (this.#pendingCount >= this.#maxConcurrent) {
- return this.#enqueueRequest(requestId, query, variables);
- }
- return this.#execute(requestId, query, variables);
- }
- reset() {
- this.#generation++;
- this.#queue = [];
- this.#pendingCount = 0;
- }
- #execute = async (requestId, query, variables) => {
- const gen = this.#generation;
- this.#pendingCount++;
- try {
- const result = await executeGraphQL(query, variables);
- if (gen === this.#generation) {
- this.#sendResponse({ requestId, ok: true, result });
- }
- }
- catch (err) {
- if (gen === this.#generation) {
- this.#sendResponse({
- requestId,
- ok: false,
- error: { message: err?.message || "GraphQL request failed" },
- });
- }
- }
- finally {
- if (gen === this.#generation) {
- this.#pendingCount--;
- this.#processRequestQueue();
- }
- }
- };
- #enqueueRequest = (requestId, query, variables) => {
- if (this.#queue.length >= this.#maxQueueSize) {
- this.#sendResponse({
- requestId,
- ok: false,
- error: { message: "Too many queued GraphQL requests" },
- });
- return Promise.resolve();
- }
- return new Promise((resolve) => {
- this.#queue.push({ requestId, query, variables, done: resolve });
- });
- };
- #processRequestQueue = () => {
- if (this.#queue.length === 0 || this.#pendingCount >= this.#maxConcurrent) {
- return;
- }
- const next = this.#queue.shift();
- this.#execute(next.requestId, next.query, next.variables).then(next.done);
- };
-}
-
-/**
- * InternalHostLwcShell
- *
- * A standard Web Component (custom element) that embeds an iframe-based widget
- * with bridge communication, sandbox management, and fullscreen support.
- *
- * Registered as ``.
- *
- * Dirty-state tracking is handled via a simple `trackdirtystate`
- * custom-event flow: the embedded widget dispatches `trackdirtystate`
- * with `{ isDirty, instanceId, label }`, and this shell re-dispatches
- * the event for the host LWC to observe.
- *
- * **How customers use this:**
- * 1. The build produces `dist/index.esm.js` which bundles this class.
- * 2. Customers copy that file into their SFDX project as an LWC entity
- * (e.g. `c/lwcShell`) and deploy it to their Salesforce org.
- * 3. A wrapper LWC imports `'c/lwcShell'` and creates ``
- * imperatively via `document.createElement('lwc-shell')`.
- *
- * See the README and the `productRegistration` demo for a full recipe.
- */
-const MAX_CONCURRENT_GRAPHQL_REQUESTS = 10;
-const MAX_GRAPHQL_QUEUE_SIZE = 50;
-const BASE_TOKENS = ["allow-scripts", "allow-pointer-lock"];
-const OPTIONAL_TOKENS = ["allow-downloads", "allow-forms", "allow-modals"];
-const BLOCKED_TOKENS = ["allow-same-origin", "allow-top-navigation", "allow-popups"];
-const STATES = {
- LOADING: "state-loading",
- LOADED: "state-loaded",
-};
-const STYLES = /* css */ `
-:host {
- display: block;
- position: relative;
- height: 100%;
- overflow: auto;
- box-sizing: border-box;
-}
-
-.container {
- width: 100%;
- height: 100%;
- position: relative;
-}
-
-.frame {
- visibility: hidden;
- display: block;
- width: 100%;
- height: 100%;
- border: none;
-}
-
-.container[data-state='state-loaded'] .frame {
- visibility: visible;
-}
-
-/* Fullscreen overlay */
-.overlayBackdrop {
- position: fixed;
- inset: 0;
- width: 100vw;
- height: 100vh;
- z-index: 9998;
- background: rgba(0, 0, 0, 0.8);
- backdrop-filter: blur(4px);
-}
-
-.overlayClose {
- position: fixed;
- top: 24px;
- right: 24px;
- z-index: 9999;
- width: 32px;
- height: 32px;
- background: rgba(0, 0, 0, 0.7);
- color: #fff;
- border-radius: 50%;
- border: none;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 16px;
- font-weight: bold;
- cursor: pointer;
-}
-
-.frameFull {
- position: fixed;
- inset: 0;
- width: 90vw;
- height: 90vh !important;
- margin: 5vh 5vw;
- z-index: 10000;
- background: #fff;
- border-radius: 8px;
- box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
-}
-`;
-class InternalHostLwcShell extends HTMLElement {
- _shadow;
- _iframe = null;
- _container = null;
- _currentState = STATES.LOADING;
- _readinessTimeout = null;
- _isFullscreen = false;
- _preFullscreenHeight = "";
- _lastThemeData = {};
- _lastPayloadData = {};
- _bridgeReady = false;
- _shellInstanceId = getUUID();
- _hasSentTheme = false;
- _hasSentData = false;
- _src = null;
- _srcdoc = null;
- _sandbox = null;
- _title = "Embedded widget";
- _view = "compact";
- _debugEnabled = true;
- _graphqlHandler;
- static get observedAttributes() {
- return ["src", "srcdoc", "sandbox", "title", "view", "debug"];
- }
- constructor() {
- super();
- this._shadow = this.attachShadow({ mode: "closed" });
- this._graphqlHandler = new GraphQLBridgeHandler((data) => {
- this._postToIframe("bridge-graphql-response", data);
- }, MAX_CONCURRENT_GRAPHQL_REQUESTS, MAX_GRAPHQL_QUEUE_SIZE);
- }
- connectedCallback() {
- this._renderInitial();
- this._setupMessageListener();
- this._log("connectedCallback");
- }
- disconnectedCallback() {
- window.removeEventListener("message", this._handleMessage);
- this._graphqlHandler.reset();
- if (this._readinessTimeout) {
- clearTimeout(this._readinessTimeout);
- this._readinessTimeout = null;
- }
- this._log("disconnectedCallback");
- }
- attributeChangedCallback(name, oldVal, newVal) {
- if (oldVal === newVal)
- return;
- switch (name) {
- case "src":
- this.src = newVal;
- break;
- case "srcdoc":
- this.srcdoc = newVal;
- break;
- case "sandbox":
- this.sandbox = newVal;
- break;
- case "title":
- this.title = newVal;
- break;
- case "view":
- this.view = newVal;
- break;
- case "debug":
- this.debug = newVal !== null;
- break;
- }
- }
- get src() {
- return this._src;
- }
- set src(v) {
- const val = v || null;
- if (this._src === val)
- return;
- this._src = val;
- if (val !== null) {
- this.setAttribute("src", val);
- }
- else {
- this.removeAttribute("src");
- }
- this._updateIframeSrc();
- }
- get srcdoc() {
- return this._srcdoc;
- }
- set srcdoc(v) {
- const val = v || null;
- if (this._srcdoc === val)
- return;
- this._srcdoc = val;
- if (val !== null) {
- this.setAttribute("srcdoc", val);
- }
- else {
- this.removeAttribute("srcdoc");
- }
- this._updateIframeSrc();
- }
- get sandbox() {
- return this._sandbox;
- }
- set sandbox(v) {
- const val = v || null;
- if (this._sandbox === val)
- return;
- this._sandbox = val;
- if (val !== null) {
- this.setAttribute("sandbox", val);
- }
- else {
- this.removeAttribute("sandbox");
- }
- this._applySandbox();
- }
- get title() {
- return this._title;
- }
- set title(v) {
- const val = v || "Embedded widget";
- if (this._title === val)
- return;
- this._title = val;
- this.setAttribute("title", val);
- this._updateTitle();
- }
- get view() {
- return this._view;
- }
- set view(v) {
- const val = v === "full" ? "full" : "compact";
- if (this._view === val)
- return;
- this._view = val;
- this.setAttribute("view", val);
- this._updateViewDOM();
- }
- /**
- * Controls debug logging. Toggle via:
- * - HTML attribute: ``
- * - Programmatically: `shell.debug = true`
- */
- get debug() {
- return this._debugEnabled;
- }
- set debug(v) {
- const val = !!v;
- if (this._debugEnabled === val)
- return;
- this._debugEnabled = val;
- if (val) {
- this.setAttribute("debug", "");
- }
- else {
- this.removeAttribute("debug");
- }
- }
- updateData(newData) {
- if (!newData || typeof newData !== "object")
- return;
- Object.entries(newData).forEach(([key, value]) => {
- const dataAttr = `data-${String(key).replace(/[A-Z]/g, "-$&").toLowerCase()}`;
- this.setAttribute(dataAttr, String(value));
- });
- const payload = this._collectDataAttributes();
- this._lastPayloadData = payload;
- if (this._bridgeReady) {
- this._postToIframe("data", payload);
- this._hasSentData = true;
- this._log("send data", payload);
- }
- else {
- this._log("queue data until bridge ready", payload);
- }
- }
- refreshTheme() {
- this._sendInitialTheme();
- }
- get _isFullView() {
- return this._view === "full";
- }
- get _frameClass() {
- return this._isFullView ? "frame frameFull" : "frame";
- }
- _log(...args) {
- if (this._debugEnabled) {
- // eslint-disable-next-line no-console
- console.log("[InternalHostLwcShell]", JSON.stringify(args, null, 2));
- }
- }
- _renderInitial() {
- const shadow = this._shadow;
- shadow.innerHTML = `
-
-
-
-
- `;
- this._container = shadow.querySelector(".container");
- this._iframe = shadow.querySelector("iframe");
- this._container.addEventListener("click", () => this._handleContainerClick());
- this._applySandbox();
- this._updateIframeSrc();
- this._sendInitialTheme();
- this._log("renderInitial: iframe ready");
- }
- _updateContainerState() {
- if (this._container) {
- this._container.setAttribute("data-state", this._currentState);
- }
- }
- _updateTitle() {
- if (this._iframe) {
- this._iframe.setAttribute("title", this._title);
- }
- if (this._container) {
- this._container.setAttribute("aria-label", this._title);
- }
- this._log("title", this._title);
- }
- _updateViewDOM() {
- if (this._iframe) {
- this._iframe.className = this._frameClass;
- }
- if (!this._container)
- return;
- const existingBackdrop = this._container.querySelector(".overlayBackdrop");
- const existingClose = this._container.querySelector(".overlayClose");
- if (existingBackdrop)
- existingBackdrop.remove();
- if (existingClose)
- existingClose.remove();
- if (this._isFullView) {
- const backdrop = document.createElement("div");
- backdrop.className = "overlayBackdrop";
- backdrop.setAttribute("aria-hidden", "true");
- const closeBtn = document.createElement("button");
- closeBtn.className = "overlayClose";
- closeBtn.type = "button";
- closeBtn.setAttribute("aria-label", "Close fullscreen");
- closeBtn.textContent = "\u2715";
- closeBtn.addEventListener("click", this._exitFullscreen);
- this._container.appendChild(backdrop);
- this._container.appendChild(closeBtn);
- }
- }
- _setState(newState) {
- this._currentState = newState;
- this._updateContainerState();
- if (newState === STATES.LOADED) {
- this._sendInitialData();
- }
- }
- _setupMessageListener() {
- window.addEventListener("message", this._handleMessage);
- }
- _handleMessage = (event) => {
- const sourceWin = this._iframe?.contentWindow;
- if (event.source !== sourceWin) {
- return;
- }
- const payload = event.data || {};
- const { type, data, id } = payload;
- if (id ? id !== this._shellInstanceId : type !== "bridge-ready")
- return;
- this._log("receive", type, data);
- if (type === "bridge-event") {
- const { eventType, detail } = data || {};
- this._log("bridge-event", eventType, detail);
- if (eventType === "resize") {
- this._handleResize(detail);
- }
- else if (eventType === "widget-ready") {
- this._handleWidgetReady();
- }
- else {
- throw new RangeError(`Invalid bridge event ${eventType}`);
- }
- }
- else if (type === "custom-event") {
- const { eventType, detail } = data || {};
- this._log("custom-event", eventType, detail);
- if (eventType === "fullscreen-request") {
- const shouldRunDefault = this.dispatchEvent(new CustomEvent(eventType, {
- detail,
- bubbles: true,
- cancelable: true,
- }));
- if (shouldRunDefault) {
- this._handleFullscreenRequest();
- }
- }
- else {
- this.dispatchEvent(new CustomEvent(eventType, { detail, bubbles: true, cancelable: true, composed: true }));
- }
- }
- else if (type === "bridge-graphql-request") {
- if (GraphQLBridgeHandler.isValidRequest(data)) {
- this._graphqlHandler.handleRequest(data);
- }
- else {
- this._graphqlHandler.rejectInvalidRequest(data);
- }
- }
- else if (type === "bridge-ready") {
- this._handleBridgeReady();
- }
- else if (type === "bridge-error") {
- this._handleBridgeError(data);
- }
- };
- _handleResize({ height }) {
- const evt = new CustomEvent("resize", {
- detail: { height },
- cancelable: true,
- });
- this.dispatchEvent(evt);
- if (!evt.defaultPrevented && !this._isFullscreen && typeof height === "number" && Number.isFinite(height)) {
- if (this._iframe)
- this._iframe.style.height = height + "px";
- this._log("applied resize", height);
- }
- }
- _handleWidgetReady() {
- if (this._readinessTimeout) {
- clearTimeout(this._readinessTimeout);
- this._readinessTimeout = null;
- }
- this.dispatchEvent(new CustomEvent("widget-ready", { bubbles: true }));
- this._log("widget-ready");
- }
- _handleFullscreenRequest() {
- if (!this._isFullscreen) {
- this._enterFullscreen();
- }
- }
- _enterFullscreen() {
- this._isFullscreen = true;
- this._preFullscreenHeight = this._iframe?.style.height ?? "";
- this._view = "full";
- this._updateViewDOM();
- if (this._iframe)
- this._iframe.style.height = "100%";
- this.updateData({ view: "full" });
- this.dispatchEvent(new CustomEvent("fullscreen-entered", {
- detail: { element: this },
- bubbles: true,
- }));
- this._log("enter fullscreen");
- }
- _exitFullscreen = () => {
- this._isFullscreen = false;
- this._view = "compact";
- this._updateViewDOM();
- if (this._iframe)
- this._iframe.style.height = this._preFullscreenHeight;
- this.updateData({ view: "compact" });
- this.dispatchEvent(new CustomEvent("fullscreen-exited", {
- detail: { element: this },
- bubbles: true,
- }));
- this._log("exit fullscreen");
- };
- _handleBridgeReady() {
- this._bridgeReady = true;
- this._postToIframe("shell-ready");
- this._setState(STATES.LOADED);
- }
- _handleBridgeError(errorData) {
- this.dispatchEvent(new CustomEvent("widget-bridge-error", { detail: errorData }));
- this._log("bridge-error", errorData);
- }
- _handleContainerClick() {
- // placeholder
- }
- _applySandbox() {
- const frame = this._iframe;
- if (!frame)
- return;
- const tokens = this._computeSandboxTokens();
- frame.setAttribute("sandbox", tokens);
- this._log("sandbox", tokens);
- }
- _computeSandboxTokens() {
- const tokens = [...BASE_TOKENS];
- if (this._sandbox) {
- const requested = String(this._sandbox).split(/\s+/).filter(Boolean);
- requested.forEach((t) => {
- if (OPTIONAL_TOKENS.includes(t) && !tokens.includes(t))
- tokens.push(t);
- if (BLOCKED_TOKENS.includes(t)) {
- this.dispatchEvent(new CustomEvent("security-violation", {
- detail: {
- type: "blocked-sandbox-token",
- token: t,
- element: this,
- },
- }));
- }
- });
- }
- return tokens.join(" ");
- }
- _updateIframeSrc() {
- const frame = this._iframe;
- if (!frame)
- return;
- // reset tracking
- this._bridgeReady = false;
- this._graphqlHandler.reset();
- this._currentState = STATES.LOADING;
- this._updateContainerState();
- // clear existing
- frame.removeAttribute("src");
- frame.removeAttribute("srcdoc");
- if (this._src) {
- frame.setAttribute("src", this._src);
- }
- else if (this._srcdoc) {
- try {
- frame.setAttribute("srcdoc", this._srcdoc);
- }
- catch {
- frame.setAttribute("src", "about:blank");
- }
- }
- // load events
- frame.onload = this._handleIframeLoad;
- frame.onerror = this._handleIframeError;
- this._log("updateIframeSrc", this._src ? "src" : this._srcdoc ? "srcdoc" : "blank");
- }
- _handleIframeLoad = () => {
- this.dispatchEvent(new CustomEvent("iframe-loaded", {
- detail: { element: this },
- bubbles: true,
- }));
- this._log("iframe-loaded");
- this._readinessTimeout = setTimeout(() => {
- if (this._currentState !== STATES.LOADED) {
- this.dispatchEvent(new CustomEvent("widget-readiness-warning", {
- detail: {
- element: this,
- message: "Widget may not be using Bridge for readiness signaling",
- },
- bubbles: true,
- }));
- this._log("widget-readiness-warning");
- }
- }, 3000);
- };
- _handleIframeError = (error) => {
- this.dispatchEvent(new CustomEvent("widget-error", {
- detail: { error, element: this },
- }));
- };
- _postToIframe(type, data) {
- const win = this._iframe?.contentWindow;
- if (!win)
- return;
- try {
- const msgType = `salesforce-${type}`;
- win.postMessage({ type: msgType, data, id: this._shellInstanceId }, "*");
- this._log("postMessage", msgType, data);
- }
- catch (e) {
- this._log("postMessage error", e instanceof Error ? e.message : String(e));
- }
- }
- _collectDataAttributes() {
- const out = {};
- for (let i = 0; i < this.attributes.length; i++) {
- const a = this.attributes[i];
- if (a.name.startsWith("data-")) {
- const raw = a.name.replace(/^data-/, "");
- // convert kebab-case to camelCase
- const camel = raw.replace(/-([a-z])/g, (_m, c) => c.toUpperCase());
- out[camel] = a.value;
- }
- }
- return out;
- }
- _sendInitialTheme() {
- const computed = getComputedStyle(this);
- const theme = {};
- for (let i = 0; i < computed.length; i++) {
- const name = computed[i];
- if (name.startsWith("--")) {
- const val = computed.getPropertyValue(name).trim();
- if (val)
- theme[name] = val;
- }
- }
- this._lastThemeData = theme;
- if (this._bridgeReady) {
- this._postToIframe("theme", theme);
- this._hasSentTheme = true;
- this._log("send theme", theme);
- }
- else {
- this._log("queue theme until bridge ready", theme);
- }
- }
- _sendInitialData() {
- if (this._lastThemeData && Object.keys(this._lastThemeData).length) {
- this._postToIframe("theme", this._lastThemeData);
- this._hasSentTheme = true;
- this._log("send theme (initial)", this._lastThemeData);
- }
- const payload = this._collectDataAttributes();
- this._lastPayloadData = { ...payload };
- this._postToIframe("data", this._lastPayloadData);
- this._hasSentData = true;
- this._log("send data (initial)", this._lastPayloadData);
- }
-}
-// ---------------------------------------------------------------------------
-// Register the custom element
-// ---------------------------------------------------------------------------
-if (!window.customElements.get("lwc-shell")) {
- window.customElements.define("lwc-shell", InternalHostLwcShell);
-}
-
-export { InternalHostLwcShell, InternalHostLwcShell as default };
-//# sourceMappingURL=index.esm.js.map
diff --git a/force-app/main/default/lwc/vendorLwcShell/vendorLwcShell.js-meta.xml b/force-app/main/default/lwc/vendorLwcShell/vendorLwcShell.js-meta.xml
deleted file mode 100644
index 9176009..0000000
--- a/force-app/main/default/lwc/vendorLwcShell/vendorLwcShell.js-meta.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
- 64.0
- false
-
diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/components/app/Navbar.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/components/app/Navbar.tsx
index c6daa51..88c6438 100644
--- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/components/app/Navbar.tsx
+++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/components/app/Navbar.tsx
@@ -13,7 +13,7 @@ const navItems = [
{ to: '/error-handling', label: 'Error Handling' },
{ to: '/styling', label: 'Styling' },
{ to: '/routing', label: 'Routing' },
- { to: '/mfe', label: 'Embedding' },
+ { to: '/embedding', label: 'Embedding' },
];
export default function Navbar() {
diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/pages/Home.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/pages/Home.tsx
index 03da58d..944261a 100644
--- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/pages/Home.tsx
+++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/pages/Home.tsx
@@ -73,10 +73,10 @@ const categories = [
'End-to-end patterns that combine multiple Salesforce APIs and React features into realistic, production-style application pages.',
},
{
- to: '/mfe',
+ to: '/embedding',
name: 'Embedding',
description:
- 'Embed an external framework app into Salesforce Lightning via lwc-shell. Covers the Platform SDK: basic embed, receiving host data, dispatching events, auto-resize, theme tokens, dirty state, and GraphQL proxying.',
+ 'Embed an external framework app into Salesforce Lightning via . Covers the Platform SDK: basic embed, receiving host props, dispatching events, auto-resize, theme tokens, and dirty state.',
},
];
@@ -237,7 +237,7 @@ export default function Home() {
icon={ }
title={HOSTING_LABEL['externally-hosted']}
blurb={
- 'Host on AWS, Heroku, or your own infra; embed via lwc-shell. Best for existing apps and SaaS vendors.'
+ 'Host on AWS, Heroku, or your own infra; embed via . Best for existing apps and SaaS vendors.'
}
onClick={() => activate('externally-hosted')}
/>
diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/pages/Mfe.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/pages/Mfe.tsx
index e98b7c8..6211e41 100644
--- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/pages/Mfe.tsx
+++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/pages/Mfe.tsx
@@ -35,14 +35,13 @@ import {
} from '@/lib/framework';
import type { Framework, Hosting } from '@/recipeRegistry';
-// MFE bridge sources (mfe-app/src/recipes/)
+// MFE guest sources (mfe-app/src/recipes/)
import basicEmbedMfe from '../../../../../../../mfe-app/src/recipes/BasicEmbed.tsx?shiki';
import receiveDataMfe from '../../../../../../../mfe-app/src/recipes/ReceiveData.tsx?shiki';
import sendEventMfe from '../../../../../../../mfe-app/src/recipes/SendEvent.tsx?shiki';
import autoResizeMfe from '../../../../../../../mfe-app/src/recipes/AutoResize.tsx?shiki';
import themeTokensMfe from '../../../../../../../mfe-app/src/recipes/ThemeTokens.tsx?shiki';
import dirtyStateMfe from '../../../../../../../mfe-app/src/recipes/DirtyState.tsx?shiki';
-import graphQLMfe from '../../../../../../../mfe-app/src/recipes/GraphQLBridge.tsx?shiki';
// LWC host JS sources (force-app/main/default/lwc/)
import basicEmbedJs from '../../../../../default/lwc/mfeBasicEmbed/mfeBasicEmbed.js?shiki=js';
@@ -51,7 +50,6 @@ import sendEventJs from '../../../../../default/lwc/mfeSendEvent/mfeSendEvent.js
import autoResizeJs from '../../../../../default/lwc/mfeAutoResize/mfeAutoResize.js?shiki=js';
import themeTokensJs from '../../../../../default/lwc/mfeThemeTokens/mfeThemeTokens.js?shiki=js';
import dirtyStateJs from '../../../../../default/lwc/mfeDirtyState/mfeDirtyState.js?shiki=js';
-import graphQLJs from '../../../../../default/lwc/mfeGraphQL/mfeGraphQL.js?shiki=js';
// LWC host HTML sources (force-app/main/default/lwc/)
import basicEmbedHtml from '../../../../../default/lwc/mfeBasicEmbed/mfeBasicEmbed.html?shiki=html';
@@ -60,7 +58,6 @@ import sendEventHtml from '../../../../../default/lwc/mfeSendEvent/mfeSendEvent.
import autoResizeHtml from '../../../../../default/lwc/mfeAutoResize/mfeAutoResize.html?shiki=html';
import themeTokensHtml from '../../../../../default/lwc/mfeThemeTokens/mfeThemeTokens.html?shiki=html';
import dirtyStateHtml from '../../../../../default/lwc/mfeDirtyState/mfeDirtyState.html?shiki=html';
-import graphQLHtml from '../../../../../default/lwc/mfeGraphQL/mfeGraphQL.html?shiki=html';
interface FlavorSources {
mfeSource: string;
@@ -83,7 +80,7 @@ const recipes: MfeRecipe[] = [
{
name: 'Basic Embed',
description:
- 'Minimum viable lwc-shell embed. The host creates the shell, sets src and sandbox, and listens for widget-ready. The MFE uses bridge.isConnected() to detect the embedding context.',
+ 'Minimum viable embed using the standard base component. The MFE resolves the Platform SDK on startup and reads chatSDK.getHostContext() to detect the embedding context.',
flavors: [
{
hosting: 'externally-hosted',
@@ -97,7 +94,7 @@ const recipes: MfeRecipe[] = [
{
name: 'Receive Data',
description:
- "Host pushes data into the MFE via shell.updateData(). The MFE receives it with bridge.addEventListener('data', handler).",
+ 'Host re-mounts with new src carrying URL query params. The MFE reads them via URLSearchParams and viewSDK.getUiProps().',
flavors: [
{
hosting: 'externally-hosted',
@@ -111,7 +108,7 @@ const recipes: MfeRecipe[] = [
{
name: 'Send Event',
description:
- 'MFE dispatches custom events to the host via bridge.dispatchEvent(). The host catches them as DOM events on the shell element.',
+ 'MFE dispatches custom events via viewSDK.dispatchEvent(name, data). Surface-level routing of those events to LWC code is host-runtime specific.',
flavors: [
{
hosting: 'externally-hosted',
@@ -125,7 +122,7 @@ const recipes: MfeRecipe[] = [
{
name: 'Auto-Resize',
description:
- 'Iframe height follows MFE content via a ResizeObserver inside the iframe. No fixed height on the shell. Cancel the resize event to opt out.',
+ 'A ResizeObserver inside the MFE tracks body height; the guest calls viewSDK.resize() to ask the host to adjust the embedding container.',
flavors: [
{
hosting: 'externally-hosted',
@@ -139,7 +136,7 @@ const recipes: MfeRecipe[] = [
{
name: 'Theme Tokens',
description:
- 'Salesforce CSS custom properties are sent to the MFE on connect. The MFE applies them to document.documentElement. Call shell.refreshTheme() to re-sync.',
+ 'MFE reads the host theme via viewSDK.getTheme() and the broader environment via chatSDK.getHostContext() (locale, displayMode, host styles).',
flavors: [
{
hosting: 'externally-hosted',
@@ -153,7 +150,7 @@ const recipes: MfeRecipe[] = [
{
name: 'Dirty State',
description:
- 'MFE notifies the host of unsaved changes via trackdirtystate events. The host can show a warning and block navigation.',
+ 'MFE calls viewSDK.markDirtyState() / clearDirtyState() to signal unsaved changes. The host surface decides how to surface the dirty signal.',
flavors: [
{
hosting: 'externally-hosted',
@@ -164,20 +161,6 @@ const recipes: MfeRecipe[] = [
},
],
},
- {
- name: 'GraphQL Bridge',
- description:
- 'MFE executes Salesforce GraphQL via bridge.graphql(). The host proxies requests — no allow-same-origin needed. Up to 10 concurrent requests with automatic queuing.',
- flavors: [
- {
- hosting: 'externally-hosted',
- framework: 'react',
- mfeSource: graphQLMfe,
- lwcJsSource: graphQLJs,
- lwcHtmlSource: graphQLHtml,
- },
- ],
- },
];
const HOSTINGS: Hosting[] = ['externally-hosted', 'salesforce-hosted'];
@@ -241,8 +224,9 @@ export default function Mfe() {
An external framework app embedded into Salesforce via{' '}
- lwc-shell. The Platform SDK (bridge)
- is the shared contract; a Salesforce-Hosted flavor is planned.
+ <lightning-embedding>. The
+ Platform SDK is the shared contract; a Salesforce-Hosted flavor is
+ planned.
diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipeRegistry.ts b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipeRegistry.ts
index f5d5dcc..f3a3927 100644
--- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipeRegistry.ts
+++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipeRegistry.ts
@@ -40,7 +40,7 @@ const CATEGORY_DEFAULTS: Record base component. The MFE detects the embedding context via chatSDK.getHostContext().',
},
{
category: 'Embedding',
- categoryRoute: '/mfe',
+ categoryRoute: '/embedding',
recipeIndex: 1,
name: 'Receive Data',
description:
- 'Pushes data from the LWC host into the MFE via shell.updateData(). The MFE receives it with bridge.addEventListener(\'data\', handler).',
+ 'Host re-mounts with new src carrying URL query params. The MFE reads them via URLSearchParams and viewSDK.getUiProps().',
},
{
category: 'Embedding',
- categoryRoute: '/mfe',
+ categoryRoute: '/embedding',
recipeIndex: 2,
name: 'Send Event',
description:
- 'The MFE dispatches custom events to the LWC host via bridge.dispatchEvent(). The host catches them on the shell element.',
+ 'The MFE dispatches custom events to the host via viewSDK.dispatchEvent(name, data). Surface-level routing is host-runtime specific.',
},
{
category: 'Embedding',
- categoryRoute: '/mfe',
+ categoryRoute: '/embedding',
recipeIndex: 3,
name: 'Auto-Resize',
description:
- 'The iframe height adjusts automatically as MFE content grows or shrinks via a ResizeObserver inside the iframe.',
+ 'A ResizeObserver inside the MFE tracks body height; the guest calls viewSDK.resize() to ask the host to adjust the embedding container.',
},
{
category: 'Embedding',
- categoryRoute: '/mfe',
+ categoryRoute: '/embedding',
recipeIndex: 4,
name: 'Theme Tokens',
description:
- 'Salesforce CSS custom properties are sent to the MFE on connect and re-synced on demand via shell.refreshTheme().',
+ 'The MFE reads the host theme via viewSDK.getTheme() and the broader environment via chatSDK.getHostContext().',
},
{
category: 'Embedding',
- categoryRoute: '/mfe',
+ categoryRoute: '/embedding',
recipeIndex: 5,
name: 'Dirty State',
description:
- 'The MFE notifies the host of unsaved changes via trackdirtystate events so the host can block navigation.',
- },
- {
- category: 'Embedding',
- categoryRoute: '/mfe',
- recipeIndex: 6,
- name: 'GraphQL Bridge',
- description:
- 'The MFE executes Salesforce GraphQL queries proxied through the host via bridge.graphql(). No allow-same-origin needed.',
+ 'The MFE calls viewSDK.markDirtyState() / clearDirtyState() to signal unsaved changes to the host surface.',
},
];
diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/README.md b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/README.md
index 1714a05..8b515c2 100644
--- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/README.md
+++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/README.md
@@ -2,7 +2,7 @@
Self-contained, single-file examples that teach one concept at a time. Every recipe inlines its GraphQL queries, mutations, types, and SDK calls so you can read the entire pattern without jumping between files. Open any `.tsx` file in `src/recipes/` and everything you need is right there.
-Each recipe is tagged by **hosting mode** (Salesforce-Hosted or Externally Hosted) and **framework** (React today; Vue/Angular planned). Categories 1–8 are Salesforce-Hosted React recipes. Category 9 (MFE) is Externally Hosted React — the framework app runs on its own server and embeds into Salesforce via `lwc-shell`.
+Each recipe is tagged by **hosting mode** (Salesforce-Hosted or Externally Hosted) and **framework** (React today; Vue/Angular planned). Categories 1–8 are Salesforce-Hosted React recipes. Category 9 (MFE) is Externally Hosted React — the framework app runs on its own server and embeds into Salesforce via the standard `` base component.
## Recommended Learning Path
@@ -16,7 +16,7 @@ Work through the categories in this order. Each builds on concepts from the prev
6. **Routing** -- React Router in UI Bundles: Link, NavLink, route parameters, nested routes, programmatic navigation
7. **Styling** -- CSS approaches: SLDS utility classes, shadcn/ui + Tailwind, Design System React components
8. **Integration** -- End-to-end patterns combining multiple APIs and React features
-9. **MFE (Externally Hosted)** -- Embed an external React app into Salesforce Lightning via `lwc-shell`. Covers the Platform SDK surface: connection detection, receiving host data, dispatching events, auto-resize, theme tokens, dirty state, and GraphQL proxying. Requires the `mfe-app/` dev server running at `localhost:4300` — see the repo README for setup steps.
+9. **MFE (Externally Hosted)** -- Embed an external React app into Salesforce Lightning via ``. Covers the Platform SDK surface: connection detection (`chatSDK.getHostContext()`), reading host props (`viewSDK.getUiProps()`), dispatching events (`viewSDK.dispatchEvent()`), resize (`viewSDK.resize()`), theme (`viewSDK.getTheme()`), and dirty state (`viewSDK.markDirtyState()` / `clearDirtyState()`). Requires the `mfe-app/` dev server running at `localhost:4300` — see the repo README for setup steps.
## Full Recipe Table
@@ -66,10 +66,9 @@ Work through the categories in this order. Each builds on concepts from the prev
| Styling | IconsDSR | Same icons as IconsSLDS via the Icon component from design-system-react. |
| Integration | SearchableAccountList | A controlled search input drives a GraphQL variable that filters Accounts by name with debounce. |
| Integration | DashboardAliasedQueries | Fetches Accounts, Contacts, and Opportunities in a single GraphQL request using aliases. |
-| MFE | BasicEmbed | Minimum viable lwc-shell embed. The MFE uses `bridge.isConnected()` to detect the embedding context. |
-| MFE | ReceiveData | Host pushes data into the MFE via `shell.updateData()`. MFE receives it with `bridge.addEventListener('data', handler)`. |
-| MFE | SendEvent | MFE dispatches custom events to the host via `bridge.dispatchEvent()`. Host catches them on the shell element. |
-| MFE | AutoResize | iframe height adjusts automatically as MFE content grows or shrinks via a ResizeObserver inside the iframe. |
-| MFE | ThemeTokens | Salesforce CSS custom properties sent to the MFE on connect and re-synced on demand via `shell.refreshTheme()`. |
-| MFE | DirtyState | MFE notifies the host of unsaved changes via `trackdirtystate` events so the host can block navigation. |
-| MFE | GraphQLBridge | MFE executes Salesforce GraphQL queries proxied through the host via `bridge.graphql()`. No `allow-same-origin` needed. |
+| MFE | BasicEmbed | Minimum viable embed using the standard `` base component. MFE uses `chatSDK.getHostContext()` to detect the embedding context. |
+| MFE | ReceiveData | Host re-mounts `` with new src carrying URL query params. MFE reads them via `URLSearchParams` and `viewSDK.getUiProps()`. |
+| MFE | SendEvent | MFE dispatches custom events via `viewSDK.dispatchEvent(name, data)`. Surface-level routing is host-runtime specific. |
+| MFE | AutoResize | A ResizeObserver inside the MFE tracks body height; the guest calls `viewSDK.resize()` to ask the host to adjust the embedding container. |
+| MFE | ThemeTokens | MFE reads the host theme via `viewSDK.getTheme()` and the broader environment via `chatSDK.getHostContext()`. |
+| MFE | DirtyState | MFE calls `viewSDK.markDirtyState()` / `clearDirtyState()` to signal unsaved changes to the host surface. |
diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/routes.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/routes.tsx
index 24b17f0..fb9f36c 100644
--- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/routes.tsx
+++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/routes.tsx
@@ -93,9 +93,9 @@ export const routes: RouteObject[] = [
handle: { showInNavigation: true, label: 'Integration' },
},
{
- path: 'mfe',
+ path: 'embedding',
element: ,
- handle: { showInNavigation: true, label: 'Mfe' },
+ handle: { showInNavigation: true, label: 'Embedding' },
},
{
path: '*',
diff --git a/mfe-app/package-lock.json b/mfe-app/package-lock.json
new file mode 100644
index 0000000..71e2647
--- /dev/null
+++ b/mfe-app/package-lock.json
@@ -0,0 +1,2607 @@
+{
+ "name": "mfe-app",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "mfe-app",
+ "version": "1.0.0",
+ "dependencies": {
+ "@salesforce/platform-sdk": "10.9.2",
+ "react": "^19.2.0",
+ "react-dom": "^19.2.0",
+ "react-router-dom": "^7.9.5"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.39.1",
+ "@types/node": "^25.0.9",
+ "@types/react": "^19.2.2",
+ "@types/react-dom": "^19.2.2",
+ "@vitejs/plugin-react": "^5.1.0",
+ "eslint": "^9.39.1",
+ "eslint-plugin-react-hooks": "^5.2.0",
+ "eslint-plugin-react-refresh": "^0.4.24",
+ "globals": "^16.5.0",
+ "typescript": "~5.9.3",
+ "typescript-eslint": "^8.46.3",
+ "vite": "^7.2.2"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-compilation-targets": "^7.28.6",
+ "@babel/helper-module-transforms": "^7.28.6",
+ "@babel/helpers": "^7.28.6",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/traverse": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.28.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.28.6",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.28.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.28.6",
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "@babel/traverse": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.28.6",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.29.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.27.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.27.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.28.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.28.6",
+ "@babel/parser": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@conduit-client/command-base": {
+ "version": "3.19.6",
+ "resolved": "https://nexus-proxy.repo.local.sfdc.net/nexus/content/groups/npm-all/@conduit-client/command-base/-/command-base-3.19.6.tgz",
+ "integrity": "sha512-zbjMSv4d0SEPR63/HTa4lFoJtMHUMQ810GprFFR9U3OcZZ4PCyUQeyM8DIja/IsRgH7HDDEL7DnhF5mpT5UxDA==",
+ "license": "SEE LICENSE IN LICENSE.txt",
+ "dependencies": {
+ "@conduit-client/utils": "3.19.6"
+ }
+ },
+ "node_modules/@conduit-client/command-cache-control": {
+ "version": "3.19.6",
+ "resolved": "https://nexus-proxy.repo.local.sfdc.net/nexus/content/groups/npm-all/@conduit-client/command-cache-control/-/command-cache-control-3.19.6.tgz",
+ "integrity": "sha512-ReZdJa5S65sGqXWocq6PkUEodon9xaBHzdpPHrdhFADotjhJY2wYQ9/050tnQ7TVg31IkeUZNNrj4WPEdYwV7w==",
+ "license": "SEE LICENSE IN LICENSE.txt",
+ "dependencies": {
+ "@conduit-client/command-base": "3.19.6",
+ "@conduit-client/service-cache-control": "3.19.6",
+ "@conduit-client/service-instrumentation": "3.19.6",
+ "@conduit-client/utils": "3.19.6"
+ }
+ },
+ "node_modules/@conduit-client/command-http-cache-control": {
+ "version": "3.19.6",
+ "resolved": "https://nexus-proxy.repo.local.sfdc.net/nexus/content/groups/npm-all/@conduit-client/command-http-cache-control/-/command-http-cache-control-3.19.6.tgz",
+ "integrity": "sha512-41bjECK4JByGKpj6chojO25i1sgOnvQoklvzgW6L9BuLE5d3uzBw6JmY5srAOiaP1t0ZYHeHm+lLnuZ7R3BjRA==",
+ "license": "SEE LICENSE IN LICENSE.txt",
+ "dependencies": {
+ "@conduit-client/command-base": "3.19.6",
+ "@conduit-client/command-cache-control": "3.19.6",
+ "@conduit-client/service-aura-network": "3.19.6",
+ "@conduit-client/service-cache-control": "3.19.6",
+ "@conduit-client/service-fetch-network": "3.19.6",
+ "@conduit-client/service-pubsub": "3.19.6",
+ "@conduit-client/utils": "3.19.6"
+ }
+ },
+ "node_modules/@conduit-client/jsonschema-validate": {
+ "version": "3.19.6",
+ "resolved": "https://nexus-proxy.repo.local.sfdc.net/nexus/content/groups/npm-all/@conduit-client/jsonschema-validate/-/jsonschema-validate-3.19.6.tgz",
+ "integrity": "sha512-XVEzbVc568gW3gu5YZh9CAVMZNjQ5hdTkR9M8ApsXX7GYhxy41Vl9F6Kkig9SFr6K0xDxAmfldvHSxkqHFebXw==",
+ "license": "SEE LICENSE IN LICENSE.txt",
+ "dependencies": {
+ "@conduit-client/utils": "3.19.6"
+ }
+ },
+ "node_modules/@conduit-client/jwt-manager": {
+ "version": "3.19.6",
+ "resolved": "https://nexus-proxy.repo.local.sfdc.net/nexus/content/groups/npm-all/@conduit-client/jwt-manager/-/jwt-manager-3.19.6.tgz",
+ "integrity": "sha512-tpkg0DtJxTdW/zac+nm8wCpSw2wavYtOfVDHfmAAtH/FF39PWjcam7ldyru1G95NR2JbvAxbLQZefC+Dt0n5kg==",
+ "license": "SEE LICENSE IN LICENSE.txt",
+ "dependencies": {
+ "jwt-decode": "~3.1.2"
+ }
+ },
+ "node_modules/@conduit-client/onestore-graphql-parser": {
+ "version": "3.19.6",
+ "resolved": "https://nexus-proxy.repo.local.sfdc.net/nexus/content/groups/npm-all/@conduit-client/onestore-graphql-parser/-/onestore-graphql-parser-3.19.6.tgz",
+ "integrity": "sha512-zz02oNZjrxWQDAt7jN3yKurCdQTp2Jz3Cp9fhkZhJ/NNUqElcQUPK/3ruUX5MhwpjbbVU4KXvNNqwjg0We7UoA==",
+ "license": "MIT",
+ "dependencies": {
+ "@conduit-client/jsonschema-validate": "3.19.6",
+ "@conduit-client/utils": "3.19.6",
+ "graphql": "16.11.0"
+ }
+ },
+ "node_modules/@conduit-client/service-aura-network": {
+ "version": "3.19.6",
+ "resolved": "https://nexus-proxy.repo.local.sfdc.net/nexus/content/groups/npm-all/@conduit-client/service-aura-network/-/service-aura-network-3.19.6.tgz",
+ "integrity": "sha512-H8+Uq0A8e9QYtYrvVfkoha7ozQP8aCTqn9vYKt1WzehkzRI+webIZjNeCrJtFsXEW+s2Shwy2P3C3GY6tF/rUg==",
+ "license": "SEE LICENSE IN LICENSE.txt",
+ "dependencies": {
+ "@conduit-client/service-instrumentation": "3.19.6",
+ "@conduit-client/utils": "3.19.6"
+ }
+ },
+ "node_modules/@conduit-client/service-cache": {
+ "version": "3.19.6",
+ "resolved": "https://nexus-proxy.repo.local.sfdc.net/nexus/content/groups/npm-all/@conduit-client/service-cache/-/service-cache-3.19.6.tgz",
+ "integrity": "sha512-WBQJgOXLZsAyWBnaOnliwgs+tx9JJsCGtLV31oqXrLbjxONk1NYNFbbpW70a8wLBJ0ZKFd4s/+nNicKwiGvRWw==",
+ "license": "SEE LICENSE IN LICENSE.txt",
+ "dependencies": {
+ "@conduit-client/service-instrumentation": "3.19.6",
+ "@conduit-client/utils": "3.19.6"
+ }
+ },
+ "node_modules/@conduit-client/service-cache-control": {
+ "version": "3.19.6",
+ "resolved": "https://nexus-proxy.repo.local.sfdc.net/nexus/content/groups/npm-all/@conduit-client/service-cache-control/-/service-cache-control-3.19.6.tgz",
+ "integrity": "sha512-pQ2ipf9FZs2vPjG3d837kpGWQGK0fmj73spn6VUWy0jiewlZZ6L/uzZzV9smMv65yAHVWnBO6ZMi/jVp+y0YsQ==",
+ "license": "SEE LICENSE IN LICENSE.txt",
+ "dependencies": {
+ "@conduit-client/service-cache": "3.19.6",
+ "@conduit-client/service-cache-inclusion-policy": "3.19.6",
+ "@conduit-client/utils": "3.19.6"
+ }
+ },
+ "node_modules/@conduit-client/service-cache-inclusion-policy": {
+ "version": "3.19.6",
+ "resolved": "https://nexus-proxy.repo.local.sfdc.net/nexus/content/groups/npm-all/@conduit-client/service-cache-inclusion-policy/-/service-cache-inclusion-policy-3.19.6.tgz",
+ "integrity": "sha512-q7kJguFTNdb5/RKykBXgDttWBwT7+tcS4zWwEaHXmDTlb4iTOlb5JfpSgFoEkx4mY1+QsiM179g4sP38IN+0jQ==",
+ "license": "SEE LICENSE IN LICENSE.txt",
+ "dependencies": {
+ "@conduit-client/service-cache": "3.19.6",
+ "@conduit-client/utils": "3.19.6"
+ }
+ },
+ "node_modules/@conduit-client/service-fetch-network": {
+ "version": "3.19.6",
+ "resolved": "https://nexus-proxy.repo.local.sfdc.net/nexus/content/groups/npm-all/@conduit-client/service-fetch-network/-/service-fetch-network-3.19.6.tgz",
+ "integrity": "sha512-yOsiRyOrM03tmX92oVbThJs7jZfjiVBQZiGkV+I+980TrJGnAwvaZlQVeG4Ncb285HtpBzkUnuc05Eol/tlCgw==",
+ "license": "SEE LICENSE IN LICENSE.txt",
+ "dependencies": {
+ "@conduit-client/jwt-manager": "3.19.6",
+ "@conduit-client/service-retry": "3.19.6",
+ "@conduit-client/utils": "3.19.6"
+ }
+ },
+ "node_modules/@conduit-client/service-instrumentation": {
+ "version": "3.19.6",
+ "resolved": "https://nexus-proxy.repo.local.sfdc.net/nexus/content/groups/npm-all/@conduit-client/service-instrumentation/-/service-instrumentation-3.19.6.tgz",
+ "integrity": "sha512-o+tR7hbwc2SNMYvKMW4IT0Z2JkO2BpCXiWchz/5N7GiKW1Xuhl/8jLY7mzmm96OVZGktPp4p4FHGlUiTdlqiSQ==",
+ "license": "SEE LICENSE IN LICENSE.txt",
+ "dependencies": {
+ "@conduit-client/service-instrumentation-noop": "3.19.6",
+ "@conduit-client/service-instrumentation-o11y": "3.19.6",
+ "@conduit-client/service-instrumentation-types": "3.19.6",
+ "@conduit-client/utils": "3.19.6",
+ "@opentelemetry/api": "1.7.0"
+ }
+ },
+ "node_modules/@conduit-client/service-instrumentation-noop": {
+ "version": "3.19.6",
+ "resolved": "https://nexus-proxy.repo.local.sfdc.net/nexus/content/groups/npm-all/@conduit-client/service-instrumentation-noop/-/service-instrumentation-noop-3.19.6.tgz",
+ "integrity": "sha512-4OuCYyNJnfDqIWXlTTxfhEkMbanIruZrMr8Rzx+MBPuYSqMSJmzCRD1Imf9nh84RedmdC4OlOxdgF6FVgYRcfQ==",
+ "license": "SEE LICENSE IN LICENSE.txt",
+ "dependencies": {
+ "@conduit-client/service-instrumentation-types": "3.19.6",
+ "@conduit-client/utils": "3.19.6",
+ "@opentelemetry/api": "1.7.0"
+ }
+ },
+ "node_modules/@conduit-client/service-instrumentation-o11y": {
+ "version": "3.19.6",
+ "resolved": "https://nexus-proxy.repo.local.sfdc.net/nexus/content/groups/npm-all/@conduit-client/service-instrumentation-o11y/-/service-instrumentation-o11y-3.19.6.tgz",
+ "integrity": "sha512-NQU9jeob5ARas8HpcyMADvhEyQk76YaBvWV6qGdQi22vPK636FCZDucysL++Q2iCSrszX5SS++0q4HEQo1mFog==",
+ "license": "SEE LICENSE IN LICENSE.txt",
+ "dependencies": {
+ "@conduit-client/service-instrumentation-types": "3.19.6",
+ "@conduit-client/utils": "3.19.6",
+ "@opentelemetry/api": "1.7.0",
+ "o11y": "252.7.0"
+ }
+ },
+ "node_modules/@conduit-client/service-instrumentation-o11y/node_modules/o11y": {
+ "version": "252.7.0",
+ "resolved": "https://nexus-proxy.repo.local.sfdc.net/nexus/content/groups/npm-all/o11y/-/o11y-252.7.0.tgz",
+ "integrity": "sha512-/P79vBumoxplQ4aqjW8GoVEMk7Uj50HOmZQAyNtu/FE8FuPXSh9rnkguxUDfN5l1oWI8Eyktrnu3XRs56OkQVQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "o11y_schema": "252.11.0",
+ "protobufjs": "7.2.4",
+ "web-vitals": "3.5.2"
+ }
+ },
+ "node_modules/@conduit-client/service-instrumentation-o11y/node_modules/o11y_schema": {
+ "version": "252.11.0",
+ "resolved": "https://nexus-proxy.repo.local.sfdc.net/nexus/content/groups/npm-all/o11y_schema/-/o11y_schema-252.11.0.tgz",
+ "integrity": "sha512-gdzht1/LRbnJZZGtR2M6ACopNLiwuUs0yP8OGnDbpCTJITwDS9S5wb7qbxUMY2kyhXkGR3oD8RY0C1S6+/1rRQ==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@conduit-client/service-instrumentation-o11y/node_modules/protobufjs": {
+ "version": "7.2.4",
+ "resolved": "https://nexus-proxy.repo.local.sfdc.net/nexus/content/groups/npm-all/protobufjs/-/protobufjs-7.2.4.tgz",
+ "integrity": "sha512-AT+RJgD2sH8phPmCf7OUZR8xGdcJRga4+1cOaXJ64hvcSkVhNcRHOwIxUatPH15+nj59WAGTDv3LSGZPEQbJaQ==",
+ "hasInstallScript": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@protobufjs/aspromise": "^1.1.2",
+ "@protobufjs/base64": "^1.1.2",
+ "@protobufjs/codegen": "^2.0.4",
+ "@protobufjs/eventemitter": "^1.1.0",
+ "@protobufjs/fetch": "^1.1.0",
+ "@protobufjs/float": "^1.0.2",
+ "@protobufjs/inquire": "^1.1.0",
+ "@protobufjs/path": "^1.1.2",
+ "@protobufjs/pool": "^1.1.0",
+ "@protobufjs/utf8": "^1.1.0",
+ "@types/node": ">=13.7.0",
+ "long": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/@conduit-client/service-instrumentation-o11y/node_modules/web-vitals": {
+ "version": "3.5.2",
+ "resolved": "https://nexus-proxy.repo.local.sfdc.net/nexus/content/groups/npm-all/web-vitals/-/web-vitals-3.5.2.tgz",
+ "integrity": "sha512-c0rhqNcHXRkY/ogGDJQxZ9Im9D19hDihbzSQJrsioex+KnFgmMzBiy57Z1EjkhX/+OjyBpclDCzz2ITtjokFmg==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@conduit-client/service-instrumentation-types": {
+ "version": "3.19.6",
+ "resolved": "https://nexus-proxy.repo.local.sfdc.net/nexus/content/groups/npm-all/@conduit-client/service-instrumentation-types/-/service-instrumentation-types-3.19.6.tgz",
+ "integrity": "sha512-BZ6BsYa2StM2+bGWnv/cB4SXaS9fCTQX1bL9z0UqIZbPmBYhtr640/SirAl8z2pqnl0Qd5OZnYAlpyLSqJoKOg==",
+ "license": "SEE LICENSE IN LICENSE.txt",
+ "dependencies": {
+ "@conduit-client/utils": "3.19.6",
+ "@opentelemetry/api": "1.7.0"
+ }
+ },
+ "node_modules/@conduit-client/service-pubsub": {
+ "version": "3.19.6",
+ "resolved": "https://nexus-proxy.repo.local.sfdc.net/nexus/content/groups/npm-all/@conduit-client/service-pubsub/-/service-pubsub-3.19.6.tgz",
+ "integrity": "sha512-HgWWiXMX43jorZNtqEHx/8cxRImcga6ycjCS6NRPnYStsndIBZIR53GnKcXbPvvaO/v5BhA3WWiWwSYe38OF2A==",
+ "license": "SEE LICENSE IN LICENSE.txt",
+ "dependencies": {
+ "@conduit-client/utils": "3.19.6"
+ }
+ },
+ "node_modules/@conduit-client/service-retry": {
+ "version": "3.19.6",
+ "resolved": "https://nexus-proxy.repo.local.sfdc.net/nexus/content/groups/npm-all/@conduit-client/service-retry/-/service-retry-3.19.6.tgz",
+ "integrity": "sha512-Zb1V2un+X3iFFN/eiY+V+G9cfvRqK2ppHjKWVF+SXRY52fOKVbEvbRA/n/YM/xemfA4PiMcmNzOrhvHwYNImaw==",
+ "license": "SEE LICENSE IN LICENSE.txt",
+ "dependencies": {
+ "@conduit-client/utils": "3.19.6"
+ }
+ },
+ "node_modules/@conduit-client/utils": {
+ "version": "3.19.6",
+ "resolved": "https://nexus-proxy.repo.local.sfdc.net/nexus/content/groups/npm-all/@conduit-client/utils/-/utils-3.19.6.tgz",
+ "integrity": "sha512-sEE8RIz1+LxPJJVkBsSATgQBEn91pKdiEahUqisnodmnct+uLbI+bi/k4NPXoNMSIZM7W2VgEWFQDHF2GXZ28w==",
+ "license": "SEE LICENSE IN LICENSE.txt"
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.27.7",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.9.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.21.2",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^2.1.7",
+ "debug": "^4.3.1",
+ "minimatch": "^3.1.5"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.4.2",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "0.17.0",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "3.3.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.14.0",
+ "debug": "^4.3.2",
+ "espree": "^10.0.1",
+ "globals": "^14.0.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.1",
+ "minimatch": "^3.1.5",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/globals": {
+ "version": "14.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "9.39.4",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "2.1.7",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.4.1",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.2",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/types": "^0.15.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.8",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.2",
+ "@humanfs/types": "^0.15.0",
+ "@humanwhocodes/retry": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/types": {
+ "version": "0.15.0",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@opentelemetry/api": {
+ "version": "1.7.0",
+ "resolved": "https://nexus-proxy.repo.local.sfdc.net/nexus/content/groups/npm-all/@opentelemetry/api/-/api-1.7.0.tgz",
+ "integrity": "sha512-AdY5wvN0P2vXBi3b29hxZgSFvdhdxPB9+f0B6s//P9Q8nibRWeA3cHm8UmLpio9ABigkVHJ5NMPk+Mz8VCCyrw==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/@protobufjs/aspromise": {
+ "version": "1.1.2",
+ "resolved": "https://nexus-proxy.repo.local.sfdc.net/nexus/content/groups/npm-all/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
+ "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/base64": {
+ "version": "1.1.2",
+ "resolved": "https://nexus-proxy.repo.local.sfdc.net/nexus/content/groups/npm-all/@protobufjs/base64/-/base64-1.1.2.tgz",
+ "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/codegen": {
+ "version": "2.0.5",
+ "resolved": "https://nexus-proxy.repo.local.sfdc.net/nexus/content/groups/npm-all/@protobufjs/codegen/-/codegen-2.0.5.tgz",
+ "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/eventemitter": {
+ "version": "1.1.1",
+ "resolved": "https://nexus-proxy.repo.local.sfdc.net/nexus/content/groups/npm-all/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz",
+ "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/fetch": {
+ "version": "1.1.1",
+ "resolved": "https://nexus-proxy.repo.local.sfdc.net/nexus/content/groups/npm-all/@protobufjs/fetch/-/fetch-1.1.1.tgz",
+ "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@protobufjs/aspromise": "^1.1.1"
+ }
+ },
+ "node_modules/@protobufjs/float": {
+ "version": "1.0.2",
+ "resolved": "https://nexus-proxy.repo.local.sfdc.net/nexus/content/groups/npm-all/@protobufjs/float/-/float-1.0.2.tgz",
+ "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/inquire": {
+ "version": "1.1.2",
+ "resolved": "https://nexus-proxy.repo.local.sfdc.net/nexus/content/groups/npm-all/@protobufjs/inquire/-/inquire-1.1.2.tgz",
+ "integrity": "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/path": {
+ "version": "1.1.2",
+ "resolved": "https://nexus-proxy.repo.local.sfdc.net/nexus/content/groups/npm-all/@protobufjs/path/-/path-1.1.2.tgz",
+ "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/pool": {
+ "version": "1.1.0",
+ "resolved": "https://nexus-proxy.repo.local.sfdc.net/nexus/content/groups/npm-all/@protobufjs/pool/-/pool-1.1.0.tgz",
+ "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/utf8": {
+ "version": "1.1.1",
+ "resolved": "https://nexus-proxy.repo.local.sfdc.net/nexus/content/groups/npm-all/@protobufjs/utf8/-/utf8-1.1.1.tgz",
+ "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-rc.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.60.2",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@salesforce/jsonrpc": {
+ "version": "10.15.1",
+ "resolved": "https://nexus-proxy.repo.local.sfdc.net/nexus/content/groups/npm-all/@salesforce/jsonrpc/-/jsonrpc-10.15.1.tgz",
+ "integrity": "sha512-EM3HrWrIEtD4o+xmOb6a24R3CNus1EBi9mgnjx1d4cV/bfu/yfkm2+4MnvPPqtue6w1VKJ2cPiPz3utbw5M3TQ==",
+ "license": "SEE LICENSE IN LICENSE.txt"
+ },
+ "node_modules/@salesforce/platform-sdk": {
+ "version": "10.9.2",
+ "resolved": "https://nexus-proxy.repo.local.sfdc.net/nexus/content/groups/npm-all/@salesforce/platform-sdk/-/platform-sdk-10.9.2.tgz",
+ "integrity": "sha512-GEMDryJgFfAfHCUpP5MaMJw80CY9/QVCuVjMvPYKCb0IP1xHT6iqwt1qeiYycuA+8MaFEmC6wN/3VLMwjacBlw==",
+ "license": "SEE LICENSE IN LICENSE.txt",
+ "dependencies": {
+ "@conduit-client/command-http-cache-control": "3.19.6",
+ "@conduit-client/onestore-graphql-parser": "3.19.6",
+ "@conduit-client/service-cache": "3.19.6",
+ "@conduit-client/service-cache-control": "3.19.6",
+ "@conduit-client/service-cache-inclusion-policy": "3.19.6",
+ "@conduit-client/service-fetch-network": "3.19.6",
+ "@conduit-client/service-pubsub": "3.19.6",
+ "@conduit-client/service-retry": "3.19.6",
+ "@conduit-client/utils": "3.19.6",
+ "@salesforce/jsonrpc": "^10.9.2",
+ "@sf-embedding/sf-embedding-bridge": "2.2.1-rc.4"
+ },
+ "peerDependencies": {
+ "o11y": ">=260.0.0",
+ "o11y_schema": ">=260.63.0"
+ }
+ },
+ "node_modules/@sf-embedding/sf-embedding-bridge": {
+ "version": "2.2.1-rc.4",
+ "resolved": "https://nexus-proxy.repo.local.sfdc.net/nexus/content/groups/npm-all/@sf-embedding/sf-embedding-bridge/-/sf-embedding-bridge-2.2.1-rc.4.tgz",
+ "integrity": "sha512-7V4kXfQ7fk40ZASIZAaaBObjwEaJO0OPR5LoczuJhUE4bg838QU58U+OBPrRJWUnfZgIKSawDTGEX5Atk+4szQ==",
+ "license": "SEE LICENSE IN LICENSE.txt",
+ "dependencies": {
+ "@salesforce/jsonrpc": "^9.13.0"
+ }
+ },
+ "node_modules/@sf-embedding/sf-embedding-bridge/node_modules/@salesforce/jsonrpc": {
+ "version": "9.21.0",
+ "resolved": "https://nexus-proxy.repo.local.sfdc.net/nexus/content/groups/npm-all/@salesforce/jsonrpc/-/jsonrpc-9.21.0.tgz",
+ "integrity": "sha512-EpUBVDpBc6bKXIVmhvdpw54r3U9oi+KLbatax9/kJyes04yxSRvchwPCHoCQkYnoE3vojCbZ2OTubg8RzAL0xQ==",
+ "license": "SEE LICENSE IN LICENSE.txt"
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "25.6.0",
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.19.0"
+ }
+ },
+ "node_modules/@types/react": {
+ "version": "19.2.14",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "19.2.3",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^19.2.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.59.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.12.2",
+ "@typescript-eslint/scope-manager": "8.59.1",
+ "@typescript-eslint/type-utils": "8.59.1",
+ "@typescript-eslint/utils": "8.59.1",
+ "@typescript-eslint/visitor-keys": "8.59.1",
+ "ignore": "^7.0.5",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.59.1",
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
+ "version": "7.0.5",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.59.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.59.1",
+ "@typescript-eslint/types": "8.59.1",
+ "@typescript-eslint/typescript-estree": "8.59.1",
+ "@typescript-eslint/visitor-keys": "8.59.1",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/project-service": {
+ "version": "8.59.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/tsconfig-utils": "^8.59.1",
+ "@typescript-eslint/types": "^8.59.1",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.59.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.59.1",
+ "@typescript-eslint/visitor-keys": "8.59.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/tsconfig-utils": {
+ "version": "8.59.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.59.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.59.1",
+ "@typescript-eslint/typescript-estree": "8.59.1",
+ "@typescript-eslint/utils": "8.59.1",
+ "debug": "^4.4.3",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.59.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.59.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/project-service": "8.59.1",
+ "@typescript-eslint/tsconfig-utils": "8.59.1",
+ "@typescript-eslint/types": "8.59.1",
+ "@typescript-eslint/visitor-keys": "8.59.1",
+ "debug": "^4.4.3",
+ "minimatch": "^10.2.2",
+ "semver": "^7.7.3",
+ "tinyglobby": "^0.2.15",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": {
+ "version": "4.0.4",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
+ "version": "5.0.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
+ "version": "10.2.5",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.5"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
+ "version": "7.7.4",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.59.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.9.1",
+ "@typescript-eslint/scope-manager": "8.59.1",
+ "@typescript-eslint/types": "8.59.1",
+ "@typescript-eslint/typescript-estree": "8.59.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.59.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.59.1",
+ "eslint-visitor-keys": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
+ "version": "5.0.1",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "5.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.29.0",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-rc.3",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.18.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.16.0",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.15.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.24",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.14",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.2",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.10.12",
+ "caniuse-lite": "^1.0.30001782",
+ "electron-to-chromium": "^1.5.328",
+ "node-releases": "^2.0.36",
+ "update-browserslist-db": "^1.2.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001791",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cookie": {
+ "version": "1.1.1",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.344",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/esbuild": {
+ "version": "0.27.7",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.27.7",
+ "@esbuild/android-arm": "0.27.7",
+ "@esbuild/android-arm64": "0.27.7",
+ "@esbuild/android-x64": "0.27.7",
+ "@esbuild/darwin-arm64": "0.27.7",
+ "@esbuild/darwin-x64": "0.27.7",
+ "@esbuild/freebsd-arm64": "0.27.7",
+ "@esbuild/freebsd-x64": "0.27.7",
+ "@esbuild/linux-arm": "0.27.7",
+ "@esbuild/linux-arm64": "0.27.7",
+ "@esbuild/linux-ia32": "0.27.7",
+ "@esbuild/linux-loong64": "0.27.7",
+ "@esbuild/linux-mips64el": "0.27.7",
+ "@esbuild/linux-ppc64": "0.27.7",
+ "@esbuild/linux-riscv64": "0.27.7",
+ "@esbuild/linux-s390x": "0.27.7",
+ "@esbuild/linux-x64": "0.27.7",
+ "@esbuild/netbsd-arm64": "0.27.7",
+ "@esbuild/netbsd-x64": "0.27.7",
+ "@esbuild/openbsd-arm64": "0.27.7",
+ "@esbuild/openbsd-x64": "0.27.7",
+ "@esbuild/openharmony-arm64": "0.27.7",
+ "@esbuild/sunos-x64": "0.27.7",
+ "@esbuild/win32-arm64": "0.27.7",
+ "@esbuild/win32-ia32": "0.27.7",
+ "@esbuild/win32-x64": "0.27.7"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "9.39.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.8.0",
+ "@eslint-community/regexpp": "^4.12.1",
+ "@eslint/config-array": "^0.21.2",
+ "@eslint/config-helpers": "^0.4.2",
+ "@eslint/core": "^0.17.0",
+ "@eslint/eslintrc": "^3.3.5",
+ "@eslint/js": "9.39.4",
+ "@eslint/plugin-kit": "^0.4.1",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "ajv": "^6.14.0",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^8.4.0",
+ "eslint-visitor-keys": "^4.2.1",
+ "espree": "^10.4.0",
+ "esquery": "^1.5.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.5",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "5.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-react-refresh": {
+ "version": "0.4.26",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "eslint": ">=8.40"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "8.4.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree": {
+ "version": "10.4.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.15.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.7.0",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.4.2",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/globals": {
+ "version": "16.5.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/graphql": {
+ "version": "16.11.0",
+ "resolved": "https://nexus-proxy.repo.local.sfdc.net/nexus/content/groups/npm-all/graphql/-/graphql-16.11.0.tgz",
+ "integrity": "sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/jwt-decode": {
+ "version": "3.1.2",
+ "resolved": "https://nexus-proxy.repo.local.sfdc.net/nexus/content/groups/npm-all/jwt-decode/-/jwt-decode-3.1.2.tgz",
+ "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==",
+ "license": "MIT"
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/long": {
+ "version": "5.3.2",
+ "resolved": "https://nexus-proxy.repo.local.sfdc.net/nexus/content/groups/npm-all/long/-/long-5.3.2.tgz",
+ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.5",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.38",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/o11y": {
+ "version": "264.15.0",
+ "resolved": "https://nexus-proxy.repo.local.sfdc.net/nexus/content/groups/npm-all/o11y/-/o11y-264.15.0.tgz",
+ "integrity": "sha512-6k8TKXrQh7NJNTjEAN+bJVLVgtyk9U2YuoujN2MgX1InUzYz0IiuZCW4/5IQVtMQu5bQWoMBf9ZyiElj5j6JBQ==",
+ "license": "BSD-3-Clause",
+ "peer": true,
+ "dependencies": {
+ "o11y_schema": "260.5.0",
+ "protobufjs": "7.5.6",
+ "web-vitals": "^5.1.0"
+ }
+ },
+ "node_modules/o11y_schema": {
+ "version": "264.86.0",
+ "resolved": "https://nexus-proxy.repo.local.sfdc.net/nexus/content/groups/npm-all/o11y_schema/-/o11y_schema-264.86.0.tgz",
+ "integrity": "sha512-aUG1amaOc8SHbW7yhzRdh/QGb2SIErQNTXx3c+g0QUxqhNDPIVuTUOkAmGWjIYuqK5tr67VTUrx0fGu7IVqr/g==",
+ "license": "BSD-3-Clause",
+ "peer": true
+ },
+ "node_modules/o11y/node_modules/o11y_schema": {
+ "version": "260.5.0",
+ "resolved": "https://nexus-proxy.repo.local.sfdc.net/nexus/content/groups/npm-all/o11y_schema/-/o11y_schema-260.5.0.tgz",
+ "integrity": "sha512-0qrtqE394CODoPovUBdQQ2efHls6Z2xP6jvLitEGKSEj1KcCyeBLx2dHwTUtv66zwZTKjSp27QLFF4PZhengEQ==",
+ "license": "BSD-3-Clause",
+ "peer": true
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.4",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.12",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/protobufjs": {
+ "version": "7.5.6",
+ "resolved": "https://nexus-proxy.repo.local.sfdc.net/nexus/content/groups/npm-all/protobufjs/-/protobufjs-7.5.6.tgz",
+ "integrity": "sha512-M71sTMB146U3u0di3yup8iM+zv8yPRNQVr1KK4tyBitl3qFvEGucq/rGDRShD2rsJhtN02RJaJ7j5X5hmy8SJg==",
+ "hasInstallScript": true,
+ "license": "BSD-3-Clause",
+ "peer": true,
+ "dependencies": {
+ "@protobufjs/aspromise": "^1.1.2",
+ "@protobufjs/base64": "^1.1.2",
+ "@protobufjs/codegen": "^2.0.5",
+ "@protobufjs/eventemitter": "^1.1.0",
+ "@protobufjs/fetch": "^1.1.0",
+ "@protobufjs/float": "^1.0.2",
+ "@protobufjs/inquire": "^1.1.1",
+ "@protobufjs/path": "^1.1.2",
+ "@protobufjs/pool": "^1.1.0",
+ "@protobufjs/utf8": "^1.1.1",
+ "@types/node": ">=13.7.0",
+ "long": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/react": {
+ "version": "19.2.5",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.2.5",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.5"
+ }
+ },
+ "node_modules/react-refresh": {
+ "version": "0.18.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-router": {
+ "version": "7.14.2",
+ "license": "MIT",
+ "dependencies": {
+ "cookie": "^1.0.1",
+ "set-cookie-parser": "^2.6.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "7.14.2",
+ "license": "MIT",
+ "dependencies": {
+ "react-router": "7.14.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.60.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.60.2",
+ "@rollup/rollup-android-arm64": "4.60.2",
+ "@rollup/rollup-darwin-arm64": "4.60.2",
+ "@rollup/rollup-darwin-x64": "4.60.2",
+ "@rollup/rollup-freebsd-arm64": "4.60.2",
+ "@rollup/rollup-freebsd-x64": "4.60.2",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.60.2",
+ "@rollup/rollup-linux-arm-musleabihf": "4.60.2",
+ "@rollup/rollup-linux-arm64-gnu": "4.60.2",
+ "@rollup/rollup-linux-arm64-musl": "4.60.2",
+ "@rollup/rollup-linux-loong64-gnu": "4.60.2",
+ "@rollup/rollup-linux-loong64-musl": "4.60.2",
+ "@rollup/rollup-linux-ppc64-gnu": "4.60.2",
+ "@rollup/rollup-linux-ppc64-musl": "4.60.2",
+ "@rollup/rollup-linux-riscv64-gnu": "4.60.2",
+ "@rollup/rollup-linux-riscv64-musl": "4.60.2",
+ "@rollup/rollup-linux-s390x-gnu": "4.60.2",
+ "@rollup/rollup-linux-x64-gnu": "4.60.2",
+ "@rollup/rollup-linux-x64-musl": "4.60.2",
+ "@rollup/rollup-openbsd-x64": "4.60.2",
+ "@rollup/rollup-openharmony-arm64": "4.60.2",
+ "@rollup/rollup-win32-arm64-msvc": "4.60.2",
+ "@rollup/rollup-win32-ia32-msvc": "4.60.2",
+ "@rollup/rollup-win32-x64-gnu": "4.60.2",
+ "@rollup/rollup-win32-x64-msvc": "4.60.2",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.27.0",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/set-cookie-parser": {
+ "version": "2.7.2",
+ "license": "MIT"
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.16",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/ts-api-utils": {
+ "version": "2.5.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.12"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4"
+ }
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/typescript-eslint": {
+ "version": "8.59.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "8.59.1",
+ "@typescript-eslint/parser": "8.59.1",
+ "@typescript-eslint/typescript-estree": "8.59.1",
+ "@typescript-eslint/utils": "8.59.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "7.19.2",
+ "license": "MIT"
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "7.3.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.27.0",
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3",
+ "postcss": "^8.5.6",
+ "rollup": "^4.43.0",
+ "tinyglobby": "^0.2.15"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "lightningcss": "^1.21.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/web-vitals": {
+ "version": "5.3.0",
+ "resolved": "https://nexus-proxy.repo.local.sfdc.net/nexus/content/groups/npm-all/web-vitals/-/web-vitals-5.3.0.tgz",
+ "integrity": "sha512-q6LWsLatGYZp5VGBIOvbTj6JBV2nOmC8KvWztXBmwJcfFAzhwKwbOxhUH306XY3CcaZDUlSmSuNPBsCn0bFu+g==",
+ "license": "Apache-2.0",
+ "peer": true
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ }
+ }
+}
diff --git a/mfe-app/package.json b/mfe-app/package.json
index f5c01ce..6c39b0c 100644
--- a/mfe-app/package.json
+++ b/mfe-app/package.json
@@ -10,7 +10,7 @@
"lint": "eslint ."
},
"dependencies": {
- "@salesforce/experimental-mfe-bridge": "2.2.1-rc.1",
+ "@salesforce/platform-sdk": "10.9.2",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-router-dom": "^7.9.5"
diff --git a/mfe-app/src/App.tsx b/mfe-app/src/App.tsx
index e4bd465..9278426 100644
--- a/mfe-app/src/App.tsx
+++ b/mfe-app/src/App.tsx
@@ -5,7 +5,6 @@ import SendEvent from './recipes/SendEvent';
import AutoResize from './recipes/AutoResize';
import ThemeTokens from './recipes/ThemeTokens';
import DirtyState from './recipes/DirtyState';
-import GraphQLBridge from './recipes/GraphQLBridge';
function Home() {
return (
@@ -24,7 +23,6 @@ function Home() {
/auto-resize
/theme-tokens
/dirty-state
- /graphql-bridge