From 9588e814291c46977b7860957b6081f856a18946 Mon Sep 17 00:00:00 2001 From: AMAN SINGH <36499868+aman06it@users.noreply.github.com> Date: Fri, 5 Jun 2026 16:03:58 +0530 Subject: [PATCH 01/10] feat: add multi-framework recipes IA, MFE recipes, and embedding scaffolding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reposition the React Recipes site as Multi-Framework Recipes per docs/multi-framework-recipes-ui-plan.md. - Registry: recipes now carry flavors[] (hosting × framework) with resolveFlavor / hasFlavor / listRecipes helpers. - Navbar: rebrand to Multi-Framework Recipes, add framework switcher (Vue/Angular disabled "(soon)", persisted to localStorage). - Home: hosting radio filter, Hosting Models explainer, footer link to the (placeholder) advanced server-recipes repo. - Standard category pages: hosting/framework chip next to the header. - Embedding (MFE): URL-driven flavor switcher (host/fw params), contextual server-repo callout when externally-hosted, all unbuilt flavors visible as disabled "(soon)" tabs. - Search: index hosting/framework keywords + category name; result rows show flavor chips. - mfe-app/: standalone Vite app (Externally-Hosted React guests). - force-app/main/default/lwc/: lwc-shell host components for the seven MFE recipes (basic embed, receive data, send event, auto-resize, theme tokens, dirty state, GraphQL bridge) plus a vendor lwc-shell shim and the Localhost CSP trusted site. Verified: tsc -b, eslint, vitest 233/233. --- README.md | 102 ++- docs/multi-framework-recipes-ui-plan.md | 383 +++++++++ .../MfeAppLocalhost.cspTrustedSite-meta.xml | 15 + .../lwc/mfeAutoResize/mfeAutoResize.html | 12 + .../lwc/mfeAutoResize/mfeAutoResize.js | 46 ++ .../mfeAutoResize/mfeAutoResize.js-meta.xml | 10 + .../lwc/mfeBasicEmbed/mfeBasicEmbed.html | 11 + .../lwc/mfeBasicEmbed/mfeBasicEmbed.js | 41 + .../mfeBasicEmbed/mfeBasicEmbed.js-meta.xml | 10 + .../lwc/mfeDirtyState/mfeDirtyState.html | 17 + .../lwc/mfeDirtyState/mfeDirtyState.js | 101 +++ .../mfeDirtyState/mfeDirtyState.js-meta.xml | 10 + .../default/lwc/mfeGraphQL/mfeGraphQL.html | 12 + .../main/default/lwc/mfeGraphQL/mfeGraphQL.js | 50 ++ .../lwc/mfeGraphQL/mfeGraphQL.js-meta.xml | 10 + .../lwc/mfeReceiveData/mfeReceiveData.html | 24 + .../lwc/mfeReceiveData/mfeReceiveData.js | 54 ++ .../mfeReceiveData/mfeReceiveData.js-meta.xml | 10 + .../lwc/mfeSendEvent/mfeSendEvent.html | 16 + .../default/lwc/mfeSendEvent/mfeSendEvent.js | 50 ++ .../lwc/mfeSendEvent/mfeSendEvent.js-meta.xml | 10 + .../lwc/mfeThemeTokens/mfeThemeTokens.html | 16 + .../lwc/mfeThemeTokens/mfeThemeTokens.js | 51 ++ .../mfeThemeTokens/mfeThemeTokens.js-meta.xml | 10 + .../lwc/vendorLwcShell/vendorLwcShell.js | 766 ++++++++++++++++++ .../vendorLwcShell/vendorLwcShell.js-meta.xml | 5 + .../uiBundles/reactRecipes/index.html | 2 +- .../uiBundles/reactRecipes/src/appLayout.tsx | 6 +- .../src/components/app/Footer.tsx | 31 + .../src/components/app/FrameworkSwitcher.tsx | 101 +++ .../src/components/app/Layout.tsx | 19 +- .../src/components/app/Navbar.tsx | 10 +- .../src/components/app/RecipeFlavorChips.tsx | 38 + .../src/components/app/SearchBar.tsx | 66 +- .../reactRecipes/src/lib/framework.ts | 110 +++ .../uiBundles/reactRecipes/src/lib/links.ts | 18 + .../uiBundles/reactRecipes/src/pages/Home.tsx | 224 ++++- .../uiBundles/reactRecipes/src/pages/Mfe.tsx | 466 +++++++++++ .../reactRecipes/src/recipeRegistry.ts | 229 +++++- .../reactRecipes/src/recipes/README.md | 14 +- .../uiBundles/reactRecipes/src/routes.tsx | 6 + .../uiBundles/reactRecipes/vite-env.d.ts | 10 + .../reactRecipes/vite-plugin-shiki.ts | 40 +- .../uiBundles/reactRecipes/vite.config.ts | 8 + mfe-app/index.html | 12 + mfe-app/package.json | 32 + mfe-app/src/App.tsx | 52 ++ mfe-app/src/index.css | 126 +++ mfe-app/src/main.tsx | 10 + mfe-app/src/recipes/AutoResize.tsx | 87 ++ mfe-app/src/recipes/BasicEmbed.tsx | 51 ++ mfe-app/src/recipes/DirtyState.tsx | 129 +++ mfe-app/src/recipes/GraphQLBridge.tsx | 124 +++ mfe-app/src/recipes/ReceiveData.tsx | 75 ++ mfe-app/src/recipes/SendEvent.tsx | 89 ++ mfe-app/src/recipes/ThemeTokens.tsx | 90 ++ mfe-app/tsconfig.app.json | 23 + mfe-app/tsconfig.json | 7 + mfe-app/tsconfig.node.json | 11 + mfe-app/vite.config.ts | 25 + 60 files changed, 4111 insertions(+), 72 deletions(-) create mode 100644 docs/multi-framework-recipes-ui-plan.md create mode 100644 force-app/main/default/cspTrustedSites/MfeAppLocalhost.cspTrustedSite-meta.xml create mode 100644 force-app/main/default/lwc/mfeAutoResize/mfeAutoResize.html create mode 100644 force-app/main/default/lwc/mfeAutoResize/mfeAutoResize.js create mode 100644 force-app/main/default/lwc/mfeAutoResize/mfeAutoResize.js-meta.xml create mode 100644 force-app/main/default/lwc/mfeBasicEmbed/mfeBasicEmbed.html create mode 100644 force-app/main/default/lwc/mfeBasicEmbed/mfeBasicEmbed.js create mode 100644 force-app/main/default/lwc/mfeBasicEmbed/mfeBasicEmbed.js-meta.xml create mode 100644 force-app/main/default/lwc/mfeDirtyState/mfeDirtyState.html create mode 100644 force-app/main/default/lwc/mfeDirtyState/mfeDirtyState.js create mode 100644 force-app/main/default/lwc/mfeDirtyState/mfeDirtyState.js-meta.xml create mode 100644 force-app/main/default/lwc/mfeGraphQL/mfeGraphQL.html create mode 100644 force-app/main/default/lwc/mfeGraphQL/mfeGraphQL.js create mode 100644 force-app/main/default/lwc/mfeGraphQL/mfeGraphQL.js-meta.xml create mode 100644 force-app/main/default/lwc/mfeReceiveData/mfeReceiveData.html create mode 100644 force-app/main/default/lwc/mfeReceiveData/mfeReceiveData.js create mode 100644 force-app/main/default/lwc/mfeReceiveData/mfeReceiveData.js-meta.xml create mode 100644 force-app/main/default/lwc/mfeSendEvent/mfeSendEvent.html create mode 100644 force-app/main/default/lwc/mfeSendEvent/mfeSendEvent.js create mode 100644 force-app/main/default/lwc/mfeSendEvent/mfeSendEvent.js-meta.xml create mode 100644 force-app/main/default/lwc/mfeThemeTokens/mfeThemeTokens.html create mode 100644 force-app/main/default/lwc/mfeThemeTokens/mfeThemeTokens.js create mode 100644 force-app/main/default/lwc/mfeThemeTokens/mfeThemeTokens.js-meta.xml create mode 100644 force-app/main/default/lwc/vendorLwcShell/vendorLwcShell.js create mode 100644 force-app/main/default/lwc/vendorLwcShell/vendorLwcShell.js-meta.xml create mode 100644 force-app/main/react-recipes/uiBundles/reactRecipes/src/components/app/Footer.tsx create mode 100644 force-app/main/react-recipes/uiBundles/reactRecipes/src/components/app/FrameworkSwitcher.tsx create mode 100644 force-app/main/react-recipes/uiBundles/reactRecipes/src/components/app/RecipeFlavorChips.tsx create mode 100644 force-app/main/react-recipes/uiBundles/reactRecipes/src/lib/framework.ts create mode 100644 force-app/main/react-recipes/uiBundles/reactRecipes/src/lib/links.ts create mode 100644 force-app/main/react-recipes/uiBundles/reactRecipes/src/pages/Mfe.tsx create mode 100644 mfe-app/index.html create mode 100644 mfe-app/package.json create mode 100644 mfe-app/src/App.tsx create mode 100644 mfe-app/src/index.css create mode 100644 mfe-app/src/main.tsx create mode 100644 mfe-app/src/recipes/AutoResize.tsx create mode 100644 mfe-app/src/recipes/BasicEmbed.tsx create mode 100644 mfe-app/src/recipes/DirtyState.tsx create mode 100644 mfe-app/src/recipes/GraphQLBridge.tsx create mode 100644 mfe-app/src/recipes/ReceiveData.tsx create mode 100644 mfe-app/src/recipes/SendEvent.tsx create mode 100644 mfe-app/src/recipes/ThemeTokens.tsx create mode 100644 mfe-app/tsconfig.app.json create mode 100644 mfe-app/tsconfig.json create mode 100644 mfe-app/tsconfig.node.json create mode 100644 mfe-app/vite.config.ts diff --git a/README.md b/README.md index b52a161..2c0f126 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,72 @@ -# Multiframework Recipes +# Multi-Framework Recipes [![CI](https://github.com/trailheadapps/multiframework-recipes/actions/workflows/ci.yml/badge.svg)](https://github.com/trailheadapps/multiframework-recipes/actions/workflows/ci.yml) [![codecov](https://codecov.io/gh/trailheadapps/multiframework-recipes/branch/main/graph/badge.svg)](https://codecov.io/gh/trailheadapps/multiframework-recipes) ![React Recipes](force-app/main/react-recipes/uiBundles/reactRecipes/react-recipes.png) -A collection of easy-to-digest code examples for building apps on the Salesforce platform using modern frontend frameworks. Each recipe demonstrates how to accomplish a specific task — from querying data with GraphQL to handling errors and navigating between views — in the fewest lines of code possible while following best practices. Each recipe includes an inline source code viewer so you can see exactly how it works. +A collection of easy-to-digest code examples for building Salesforce apps with modern frontend frameworks. Every recipe teaches one concept — querying data with GraphQL, handling errors, navigating between views, embedding an external app — in the fewest lines of code possible while following best practices. Each recipe includes an inline source code viewer so you can see exactly how it works. -This sample application is designed to run on the Salesforce Platform. It covers what a frontend developer needs to know about Salesforce, and what a Salesforce developer needs to know about modern frameworks — taught at the intersection. +The repo covers both axes a modern Salesforce frontend developer needs to navigate: -> Multi-Framework currently supports **React**, with additional frameworks coming over time. The feature is in Beta and only available in Scratch Orgs and Sandboxes — not yet in Developer Edition orgs or Trailhead Playgrounds. +- **Framework:** React today, with Vue and Angular planned. Same recipes, different implementations. +- **Hosting mode:** Whether you run the app on your own infrastructure or ship it to the Salesforce platform. + +> **Status:** React is the only framework implemented today. Vue and Angular are planned. Multi-Framework is in Beta and available in Scratch Orgs and Sandboxes — not yet in Developer Edition orgs or Trailhead Playgrounds. > -> There is a known limitation where orgs that do not use English (`en_US`) as the default language may experience issues. The scratch org definition in this project explicitly sets `"language": "en_US"` to work around this. If you are using a sandbox or other org type, ensure the default language is set to English. +> **Known limitation:** orgs that do not use English (`en_US`) as the default language may experience issues. The scratch org definition in this project explicitly sets `"language": "en_US"` to work around this. On sandbox or other org types, ensure the default language is English. **Learn more:** Read the [Salesforce Multi-Framework developer guide](https://developer.salesforce.com/docs/platform/einstein-for-devs/guide/reactdev-overview.html) for a comprehensive overview. -## Architecture +## The Two Hosting Modes + +Every recipe in this repo falls into one of two hosting modes. The framework code is similar; the deployment, distribution, and integration story is very different. + +### Salesforce-Hosted + +The framework app is built as a **Salesforce UI Bundle** and deployed directly to the org. Salesforce serves the static assets from the platform; no external hosting required. ```mermaid graph LR - A[React App
Vite + TypeScript] -->|Build| B[UI Bundle] + A[Framework App
Vite + TypeScript] -->|Build| B[UI Bundle] B -->|Deploy| C[Salesforce Org] C -->|Query| D[GraphQL UIAPI] C -->|Fetch| E[REST APIs] ``` +**Use when:** you want a single-team workflow, zero external infrastructure, and deep integration with Salesforce's security/identity model. + +Today this mode lives in [`force-app/main/react-recipes/uiBundles/reactRecipes/`](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. + +```mermaid +graph LR + A[External Framework App
mfe-app/] -->|iframe src| B[lwc-shell] + 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 +``` + +**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. + +Today this mode lives in [`mfe-app/`](mfe-app/) (the external React app) plus [`force-app/main/default/lwc/mfe*`](force-app/main/default/lwc/) (the LWC host components). + +### The Platform SDK: one API, both modes + +Both hosting modes use the **Platform SDK** as the unifying contract — it's how the framework app reads Salesforce data, dispatches events to the host, and stays in sync with org-level theme tokens. The SDK surface is the same whether the app is Salesforce-hosted or externally hosted; only the transport underneath differs. Learning the SDK is the portable skill this repo teaches. + ## Table of Contents - [Setting up a Scratch Org](#setting-up-a-scratch-org) - [Setting up a Sandbox](#setting-up-a-sandbox) - [Developer Edition](#developer-edition) -- [Install & Deploy React Recipes](#install--deploy-react-recipes) +- [Install & Deploy Salesforce-Hosted Recipes](#install--deploy-salesforce-hosted-recipes) +- [Install & Run Externally Hosted Recipes](#install--run-externally-hosted-recipes) - [Local Development](#local-development) - [Testing](#testing) - [Optional installation instructions](#optional-installation-instructions) @@ -138,7 +174,9 @@ graph LR Developer Edition support is coming soon. -## Install & Deploy React Recipes +## Install & Deploy Salesforce-Hosted Recipes + +These recipes run as a Salesforce UI Bundle served directly from the org. Today the only implementation is React; Vue and Angular are planned. 1. Install dependencies, fetch the GraphQL schema, and run codegen: @@ -168,6 +206,52 @@ Developer Edition support is coming soon. sf org open ``` +## 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. + +> **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. + +1. Install dependencies for the external app: + + ```bash + cd mfe-app + npm install + ``` + +1. Start the external app's dev server: + + ```bash + npm run dev + ``` + + The app starts at `http://localhost:4300`. Keep this running while using the externally hosted recipes in your org. + +1. Deploy the LWC host components: + + ```bash + cd .. + sf project deploy start -d force-app/main/default/lwc + ``` + +1. Open the scratch org, go to App Launcher, and search for any of the host components (`mfeBasicEmbed`, `mfeReceiveData`, etc.) to add them to a Lightning page. + +### Available externally hosted recipes + +| 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 | + +### Pointing at a deployed external app + +For production or sandbox use, deploy the external app to a hosted URL and set the `baseUrl` property on each LWC host component to point at that URL instead of `localhost:4300`. + ## Local Development Start the Vite development server with hot module replacement: diff --git a/docs/multi-framework-recipes-ui-plan.md b/docs/multi-framework-recipes-ui-plan.md new file mode 100644 index 0000000..2abd969 --- /dev/null +++ b/docs/multi-framework-recipes-ui-plan.md @@ -0,0 +1,383 @@ +# 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. | diff --git a/force-app/main/default/cspTrustedSites/MfeAppLocalhost.cspTrustedSite-meta.xml b/force-app/main/default/cspTrustedSites/MfeAppLocalhost.cspTrustedSite-meta.xml new file mode 100644 index 0000000..0987ccd --- /dev/null +++ b/force-app/main/default/cspTrustedSites/MfeAppLocalhost.cspTrustedSite-meta.xml @@ -0,0 +1,15 @@ + + + false + false + All + Local MFE app dev server — allows the lwc-shell iframe to load from localhost:4300 + http://localhost:4300 + true + false + false + true + false + false + false + diff --git a/force-app/main/default/lwc/mfeAutoResize/mfeAutoResize.html b/force-app/main/default/lwc/mfeAutoResize/mfeAutoResize.html new file mode 100644 index 0000000..b5e5c03 --- /dev/null +++ b/force-app/main/default/lwc/mfeAutoResize/mfeAutoResize.html @@ -0,0 +1,12 @@ + diff --git a/force-app/main/default/lwc/mfeAutoResize/mfeAutoResize.js b/force-app/main/default/lwc/mfeAutoResize/mfeAutoResize.js new file mode 100644 index 0000000..58b1400 --- /dev/null +++ b/force-app/main/default/lwc/mfeAutoResize/mfeAutoResize.js @@ -0,0 +1,46 @@ +import { LightningElement, api } from 'lwc'; +import 'c/vendorLwcShell'; + +export default class MfeAutoResize extends LightningElement { + @api baseUrl = 'http://localhost:4300'; + _shellElement; + + 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/mfeAutoResize/mfeAutoResize.js-meta.xml b/force-app/main/default/lwc/mfeAutoResize/mfeAutoResize.js-meta.xml new file mode 100644 index 0000000..620831e --- /dev/null +++ b/force-app/main/default/lwc/mfeAutoResize/mfeAutoResize.js-meta.xml @@ -0,0 +1,10 @@ + + + 66.0 + true + + lightning__AppPage + lightning__RecordPage + lightning__HomePage + + diff --git a/force-app/main/default/lwc/mfeBasicEmbed/mfeBasicEmbed.html b/force-app/main/default/lwc/mfeBasicEmbed/mfeBasicEmbed.html new file mode 100644 index 0000000..89cc3b6 --- /dev/null +++ b/force-app/main/default/lwc/mfeBasicEmbed/mfeBasicEmbed.html @@ -0,0 +1,11 @@ + diff --git a/force-app/main/default/lwc/mfeBasicEmbed/mfeBasicEmbed.js b/force-app/main/default/lwc/mfeBasicEmbed/mfeBasicEmbed.js new file mode 100644 index 0000000..62652a4 --- /dev/null +++ b/force-app/main/default/lwc/mfeBasicEmbed/mfeBasicEmbed.js @@ -0,0 +1,41 @@ +import { LightningElement, api } from 'lwc'; +import 'c/vendorLwcShell'; + +export default class MfeBasicEmbed extends LightningElement { + @api baseUrl = 'http://localhost:4300'; + _shellElement; + + 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/mfeBasicEmbed/mfeBasicEmbed.js-meta.xml b/force-app/main/default/lwc/mfeBasicEmbed/mfeBasicEmbed.js-meta.xml new file mode 100644 index 0000000..620831e --- /dev/null +++ b/force-app/main/default/lwc/mfeBasicEmbed/mfeBasicEmbed.js-meta.xml @@ -0,0 +1,10 @@ + + + 66.0 + true + + lightning__AppPage + lightning__RecordPage + lightning__HomePage + + diff --git a/force-app/main/default/lwc/mfeDirtyState/mfeDirtyState.html b/force-app/main/default/lwc/mfeDirtyState/mfeDirtyState.html new file mode 100644 index 0000000..8b3353c --- /dev/null +++ b/force-app/main/default/lwc/mfeDirtyState/mfeDirtyState.html @@ -0,0 +1,17 @@ + diff --git a/force-app/main/default/lwc/mfeDirtyState/mfeDirtyState.js b/force-app/main/default/lwc/mfeDirtyState/mfeDirtyState.js new file mode 100644 index 0000000..1cdbb2e --- /dev/null +++ b/force-app/main/default/lwc/mfeDirtyState/mfeDirtyState.js @@ -0,0 +1,101 @@ +import { LightningElement, api, track } from 'lwc'; +import 'c/vendorLwcShell'; + +export default class MfeDirtyState extends LightningElement { + @api baseUrl = 'http://localhost:4300'; + @track isDirty = false; + @track dirtyLabel = ''; + _shellElement; + _beforeUnloadHandler; + + 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/mfeDirtyState/mfeDirtyState.js-meta.xml b/force-app/main/default/lwc/mfeDirtyState/mfeDirtyState.js-meta.xml new file mode 100644 index 0000000..620831e --- /dev/null +++ b/force-app/main/default/lwc/mfeDirtyState/mfeDirtyState.js-meta.xml @@ -0,0 +1,10 @@ + + + 66.0 + true + + lightning__AppPage + lightning__RecordPage + lightning__HomePage + + diff --git a/force-app/main/default/lwc/mfeGraphQL/mfeGraphQL.html b/force-app/main/default/lwc/mfeGraphQL/mfeGraphQL.html new file mode 100644 index 0000000..68cfcaa --- /dev/null +++ b/force-app/main/default/lwc/mfeGraphQL/mfeGraphQL.html @@ -0,0 +1,12 @@ + diff --git a/force-app/main/default/lwc/mfeGraphQL/mfeGraphQL.js b/force-app/main/default/lwc/mfeGraphQL/mfeGraphQL.js new file mode 100644 index 0000000..1a26acb --- /dev/null +++ b/force-app/main/default/lwc/mfeGraphQL/mfeGraphQL.js @@ -0,0 +1,50 @@ +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 new file mode 100644 index 0000000..620831e --- /dev/null +++ b/force-app/main/default/lwc/mfeGraphQL/mfeGraphQL.js-meta.xml @@ -0,0 +1,10 @@ + + + 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 new file mode 100644 index 0000000..602042f --- /dev/null +++ b/force-app/main/default/lwc/mfeReceiveData/mfeReceiveData.html @@ -0,0 +1,24 @@ + diff --git a/force-app/main/default/lwc/mfeReceiveData/mfeReceiveData.js b/force-app/main/default/lwc/mfeReceiveData/mfeReceiveData.js new file mode 100644 index 0000000..7b144b3 --- /dev/null +++ b/force-app/main/default/lwc/mfeReceiveData/mfeReceiveData.js @@ -0,0 +1,54 @@ +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; + } + + 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() }); + } + + _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() }); + }); + container.appendChild(shell); + this._shellElement = shell; + } +} diff --git a/force-app/main/default/lwc/mfeReceiveData/mfeReceiveData.js-meta.xml b/force-app/main/default/lwc/mfeReceiveData/mfeReceiveData.js-meta.xml new file mode 100644 index 0000000..620831e --- /dev/null +++ b/force-app/main/default/lwc/mfeReceiveData/mfeReceiveData.js-meta.xml @@ -0,0 +1,10 @@ + + + 66.0 + true + + lightning__AppPage + lightning__RecordPage + lightning__HomePage + + diff --git a/force-app/main/default/lwc/mfeSendEvent/mfeSendEvent.html b/force-app/main/default/lwc/mfeSendEvent/mfeSendEvent.html new file mode 100644 index 0000000..4bd4072 --- /dev/null +++ b/force-app/main/default/lwc/mfeSendEvent/mfeSendEvent.html @@ -0,0 +1,16 @@ + diff --git a/force-app/main/default/lwc/mfeSendEvent/mfeSendEvent.js b/force-app/main/default/lwc/mfeSendEvent/mfeSendEvent.js new file mode 100644 index 0000000..f52fb8f --- /dev/null +++ b/force-app/main/default/lwc/mfeSendEvent/mfeSendEvent.js @@ -0,0 +1,50 @@ +import { LightningElement, api, track } from 'lwc'; +import 'c/vendorLwcShell'; + +export default class MfeSendEvent extends LightningElement { + @api baseUrl = 'http://localhost:4300'; + @track lastAction = ''; + _shellElement; + + 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/mfeSendEvent/mfeSendEvent.js-meta.xml b/force-app/main/default/lwc/mfeSendEvent/mfeSendEvent.js-meta.xml new file mode 100644 index 0000000..620831e --- /dev/null +++ b/force-app/main/default/lwc/mfeSendEvent/mfeSendEvent.js-meta.xml @@ -0,0 +1,10 @@ + + + 66.0 + true + + lightning__AppPage + lightning__RecordPage + lightning__HomePage + + diff --git a/force-app/main/default/lwc/mfeThemeTokens/mfeThemeTokens.html b/force-app/main/default/lwc/mfeThemeTokens/mfeThemeTokens.html new file mode 100644 index 0000000..674fa4e --- /dev/null +++ b/force-app/main/default/lwc/mfeThemeTokens/mfeThemeTokens.html @@ -0,0 +1,16 @@ + diff --git a/force-app/main/default/lwc/mfeThemeTokens/mfeThemeTokens.js b/force-app/main/default/lwc/mfeThemeTokens/mfeThemeTokens.js new file mode 100644 index 0000000..04fadfb --- /dev/null +++ b/force-app/main/default/lwc/mfeThemeTokens/mfeThemeTokens.js @@ -0,0 +1,51 @@ +import { LightningElement, api } from 'lwc'; +import 'c/vendorLwcShell'; + +export default class MfeThemeTokens extends LightningElement { + @api baseUrl = 'http://localhost:4300'; + _shellElement; + + 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/mfeThemeTokens/mfeThemeTokens.js-meta.xml b/force-app/main/default/lwc/mfeThemeTokens/mfeThemeTokens.js-meta.xml new file mode 100644 index 0000000..620831e --- /dev/null +++ b/force-app/main/default/lwc/mfeThemeTokens/mfeThemeTokens.js-meta.xml @@ -0,0 +1,10 @@ + + + 66.0 + true + + lightning__AppPage + lightning__RecordPage + lightning__HomePage + + diff --git a/force-app/main/default/lwc/vendorLwcShell/vendorLwcShell.js b/force-app/main/default/lwc/vendorLwcShell/vendorLwcShell.js new file mode 100644 index 0000000..ad3fb3a --- /dev/null +++ b/force-app/main/default/lwc/vendorLwcShell/vendorLwcShell.js @@ -0,0 +1,766 @@ +/* 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 new file mode 100644 index 0000000..9176009 --- /dev/null +++ b/force-app/main/default/lwc/vendorLwcShell/vendorLwcShell.js-meta.xml @@ -0,0 +1,5 @@ + + + 64.0 + false + diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/index.html b/force-app/main/react-recipes/uiBundles/reactRecipes/index.html index 44cdeaa..3b6f840 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/index.html +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/index.html @@ -4,7 +4,7 @@ - React Recipes + Multi-Framework Recipes
diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/appLayout.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/appLayout.tsx index c679197..2585604 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/appLayout.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/appLayout.tsx @@ -1,13 +1,15 @@ import { Outlet } from 'react-router'; import Navbar from '@/components/app/Navbar'; +import Footer from '@/components/app/Footer'; export default function AppLayout() { return ( -
+
-
+
+
); } diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/components/app/Footer.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/components/app/Footer.tsx new file mode 100644 index 0000000..80b59b0 --- /dev/null +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/components/app/Footer.tsx @@ -0,0 +1,31 @@ +import { ExternalLink } from 'lucide-react'; +import { EXTERNAL_LINKS } from '@/lib/links'; + +export default function Footer() { + const repo = EXTERNAL_LINKS.serverRepo; + return ( + + ); +} diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/components/app/FrameworkSwitcher.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/components/app/FrameworkSwitcher.tsx new file mode 100644 index 0000000..1a1720a --- /dev/null +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/components/app/FrameworkSwitcher.tsx @@ -0,0 +1,101 @@ +import { useEffect, useRef, useState } from 'react'; +import { Check, ChevronDown } from 'lucide-react'; +import { + FRAMEWORKS, + FRAMEWORK_LABEL, + SOON_LABEL, + isFrameworkEnabled, + useFramework, +} from '@/lib/framework'; +import { cn } from '@/lib/utils'; + +export default function FrameworkSwitcher() { + const [framework, setFramework] = useFramework(); + const [open, setOpen] = useState(false); + const triggerRef = useRef(null); + const listRef = useRef(null); + + useEffect(() => { + if (!open) return; + function onClick(e: MouseEvent) { + const target = e.target as Node; + if ( + triggerRef.current?.contains(target) || + listRef.current?.contains(target) + ) { + return; + } + setOpen(false); + } + function onKey(e: KeyboardEvent) { + if (e.key === 'Escape') setOpen(false); + } + document.addEventListener('mousedown', onClick); + document.addEventListener('keydown', onKey); + return () => { + document.removeEventListener('mousedown', onClick); + document.removeEventListener('keydown', onKey); + }; + }, [open]); + + return ( +
+ + + {open && ( +
+ {FRAMEWORKS.map((f) => { + const enabled = isFrameworkEnabled(f); + const selected = f === framework; + return ( + + ); + })} +
+ )} +
+ ); +} diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/components/app/Layout.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/components/app/Layout.tsx index f9baf73..5d499f4 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/components/app/Layout.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/components/app/Layout.tsx @@ -1,5 +1,5 @@ import { useState, type ReactNode } from 'react'; -import { useSearchParams } from 'react-router'; +import { useLocation, useSearchParams } from 'react-router'; import { Card, CardHeader, @@ -9,6 +9,11 @@ import { } from '@/components/ui/card'; import { cn } from '@/lib/utils'; import CodeBlock from './CodeBlock'; +import RecipeFlavorChips from './RecipeFlavorChips'; +import { + getCategoryFramework, + getCategoryHosting, +} from '@/recipeRegistry'; export interface RecipeItem { name: string; description?: string; @@ -22,6 +27,11 @@ interface LayoutProps { } export default function Layout({ header, recipes = [] }: LayoutProps) { + const { pathname } = useLocation(); + const hosting = getCategoryHosting(pathname); + const framework = getCategoryFramework(pathname); + const categoryFlavors = + hosting && framework ? [{ hosting, framework }] : []; const [searchParams, setSearchParams] = useSearchParams(); // Derive initial index from ?recipe param; track the param we last consumed // to detect new search navigations without using setState in an effect. @@ -62,7 +72,12 @@ export default function Layout({ header, recipes = [] }: LayoutProps) { {/* Page Header */} {header && (
-

{header}

+
+

{header}

+ {categoryFlavors.length > 0 && ( + + )} +
)} 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 e5708ba..c6daa51 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 @@ -2,6 +2,7 @@ import { useLocation, useNavigate } from 'react-router'; import { Code2 } from 'lucide-react'; import { cn } from '@/lib/utils'; import SearchBar from './SearchBar'; +import FrameworkSwitcher from './FrameworkSwitcher'; const navItems = [ { to: '/hello', label: 'Hello' }, @@ -12,6 +13,7 @@ const navItems = [ { to: '/error-handling', label: 'Error Handling' }, { to: '/styling', label: 'Styling' }, { to: '/routing', label: 'Routing' }, + { to: '/mfe', label: 'Embedding' }, ]; export default function Navbar() { @@ -20,15 +22,19 @@ export default function Navbar() { return (
-
+
+ +
+ {/* Hosting filter */} +
+ Hosting: +
+ {HOSTING_FILTER_OPTIONS.map((option, i) => { + const active = filter === option.value; + return ( + + ); + })} +
+
+ {/* Category tiles */}
- {categories.map(cat => ( - - ))} + {visibleCategories.map((cat) => { + const hosting = getCategoryHosting(cat.to); + const fw = getCategoryFramework(cat.to); + const flavors = + hosting && fw ? [{ hosting, framework: fw }] : []; + return ( + + ); + })} + {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 ( + + ); +} + 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 */} + + + {/* 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 && ( +
+ + + Externally hosted: this recipe assumes the guest app runs on a + third-party domain. For local dev with SSL, see{' '} + + {EXTERNAL_LINKS.serverRepo.label} + {EXTERNAL_LINKS.serverRepo.placeholder + ? ' (coming soon)' + : ''} + + + . + +
+ )} +
+ + + + + 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 ( + + ); +} + +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. +

+ +
+ + +
+ +
+

{items.length} item{items.length !== 1 ? 's' : ''}

+ {items.length === 0 ? ( +

No items — iframe should be at minimum height.

+ ) : ( +
    + {items.map(item => ( +
  • + {item.text} + +
  • + ))} +
+ )} +
+
+ ); +} 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.
+ )} + +
+ + + +
+ +
+ + +
+
+ ); +} 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. +

+ + + + {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 => ( + + ))} +
+
+ + {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…

+ ) : ( + + + + + + + + + {tokenEntries.map(([prop, value]) => ( + + + + + ))} + +
PropertyValue
{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).

- 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 @@ - 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.

- -
+
+ +
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
  • @@ -42,7 +40,6 @@ function App() { } /> } /> } /> - } /> } /> diff --git a/mfe-app/src/main.tsx b/mfe-app/src/main.tsx index 7825095..60d77e9 100644 --- a/mfe-app/src/main.tsx +++ b/mfe-app/src/main.tsx @@ -1,10 +1,22 @@ +// Side-effect import: registers the sf-embedding bootstrap listener at +// module-load time, before any createXxxSDK() call below. Required when +// the app is loaded inside a iframe; harmless when +// the app runs standalone in a plain browser tab. +import '@salesforce/platform-sdk/sf-embedding'; + import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; +import { createChatSDK, createViewSDK } from '@salesforce/platform-sdk'; import './index.css'; import App from './App.tsx'; +import { SdkProvider } from './sdk-context.tsx'; + +const [chat, view] = await Promise.all([createChatSDK(), createViewSDK()]); createRoot(document.getElementById('root')!).render( - + + + , ); diff --git a/mfe-app/src/recipes/AutoResize.tsx b/mfe-app/src/recipes/AutoResize.tsx index 369910b..26372c0 100644 --- a/mfe-app/src/recipes/AutoResize.tsx +++ b/mfe-app/src/recipes/AutoResize.tsx @@ -1,18 +1,20 @@ /** * 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. + * Demonstrates calling viewSDK.resize() to ask the host to resize the + * embedding container. A ResizeObserver tracks document.body's height and + * forwards the new size to the host on every change. * - * 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. + * Key concept: viewSDK.resize(width, height) accepts CSS-style strings + * ("auto", "100%", "320px"). Pass "auto" for either axis to let the host + * compute its own size, or a specific value to pin it. Unlike the legacy + * bridge — which auto-pushed body height — the SDK is explicit: you decide + * when (and what dimensions) to send. * * @see ThemeTokens — receiving Salesforce design tokens */ -import { useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; +import { useSdk } from '../sdk-context'; interface Item { id: number; @@ -26,7 +28,22 @@ function makeItem(): Item { } export default function AutoResize() { + const { view } = useSdk(); const [items, setItems] = useState([makeItem(), makeItem()]); + const containerRef = useRef(null); + + useEffect(() => { + if (!view.resize || !containerRef.current) return; + + const observer = new ResizeObserver(entries => { + const height = entries[0]?.contentRect.height; + if (height != null) { + void view.resize?.('auto', `${Math.ceil(height)}px`); + } + }); + observer.observe(document.body); + return () => observer.disconnect(); + }, [view]); function addItem() { setItems(prev => [...prev, makeItem()]); @@ -37,12 +54,11 @@ export default function AutoResize() { } 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 or remove items — a ResizeObserver tracks body height and calls{' '} + viewSDK.resize() so the host iframe matches the content.

    diff --git a/mfe-app/src/recipes/BasicEmbed.tsx b/mfe-app/src/recipes/BasicEmbed.tsx index 38af6a0..203f365 100644 --- a/mfe-app/src/recipes/BasicEmbed.tsx +++ b/mfe-app/src/recipes/BasicEmbed.tsx @@ -1,50 +1,50 @@ /** * Basic Embed * - * The minimum viable MFE. Checks whether the bridge is connected on mount - * and displays the connection status. + * The minimum viable MFE. Resolves the Platform SDK once at app startup and + * renders the connection state plus the host context the SDK was given. * - * 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. + * On the Salesforce side the host LWC declares pointing + * at this route. The SDK auto-initialises via the side-effect import in + * main.tsx before any component renders. * - * 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. + * Key concept: createChatSDK() / createViewSDK() resolve once on app load. + * The result is shared via SdkProvider so every recipe consumes the same + * instance. When running standalone (no iframe), the SDKs still resolve but + * most methods are no-ops; surface detection drops to "WebApp". * - * @see ReceiveData — receiving host data via the bridge + * @see ReceiveData — receiving host data via viewSDK.getUiProps() */ -import { useEffect, useState } from 'react'; -import bridge from '@salesforce/experimental-mfe-bridge'; +import { useSdk } from '../sdk-context'; 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()); - }, []); + const { chat } = useSdk(); + const hostContext = chat.getHostContext?.() ?? {}; + const connected = Object.keys(hostContext).length > 0; return (

    Basic Embed

    - Detects whether this React app is running inside a Salesforce lwc-shell iframe. + Detects whether this React app is running inside a Salesforce{' '} + <lightning-embedding> iframe.

    -

    Bridge status

    +

    SDK status

    {connected ? 'Connected — running inside Salesforce' : 'Not connected — running standalone'}

    -

    Instance ID

    -

    - {connected ? bridge.instanceId : '—'} -

    +

    Host context

    + {connected ? ( +
    +                        {JSON.stringify(hostContext, null, 2)}
    +                    
    + ) : ( +

    + )}
    ); diff --git a/mfe-app/src/recipes/DirtyState.tsx b/mfe-app/src/recipes/DirtyState.tsx index 8cb2c3e..3ed563c 100644 --- a/mfe-app/src/recipes/DirtyState.tsx +++ b/mfe-app/src/recipes/DirtyState.tsx @@ -2,17 +2,17 @@ * 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. + * may show a "you have unsaved changes" warning before navigation. * - * 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. + * Key concept: viewSDK.markDirtyState() and viewSDK.clearDirtyState() are + * the explicit dirty-state API. Call markDirtyState() whenever the form + * mutates and clearDirtyState() after a successful save (or discard). The + * host decides how to surface the state — typically a navigation guard. * * @see SendEvent — dispatching generic events to the host */ -import { useState, useEffect } from 'react'; -import bridge from '@salesforce/experimental-mfe-bridge'; +import { useEffect, useState } from 'react'; +import { useSdk } from '../sdk-context'; interface FormState { name: string; @@ -27,6 +27,7 @@ function isDirtyCheck(current: FormState, saved: FormState) { } export default function DirtyState() { + const { view } = useSdk(); const [form, setForm] = useState(INITIAL); const [saved, setSaved] = useState(INITIAL); const [isDirty, setIsDirty] = useState(false); @@ -35,17 +36,12 @@ export default function DirtyState() { 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]); + if (dirty) { + void view.markDirtyState?.(); + } else { + void view.clearDirtyState?.(); + } + }, [form, saved, view]); function handleChange(field: keyof FormState, value: string) { setForm(prev => ({ ...prev, [field]: value })); @@ -64,8 +60,8 @@ export default function DirtyState() {

    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. + viewSDK.markDirtyState() and cleared with{' '} + clearDirtyState() on save.

    {isDirty && ( diff --git a/mfe-app/src/recipes/GraphQLBridge.tsx b/mfe-app/src/recipes/GraphQLBridge.tsx deleted file mode 100644 index 3db9a00..0000000 --- a/mfe-app/src/recipes/GraphQLBridge.tsx +++ /dev/null @@ -1,124 +0,0 @@ -/** - * 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. -

    - - - - {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 index 1b1f84e..9148058 100644 --- a/mfe-app/src/recipes/ReceiveData.tsx +++ b/mfe-app/src/recipes/ReceiveData.tsx @@ -1,41 +1,58 @@ /** * 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. + * Listens for UI props pushed from the Salesforce LWC host. The host LWC + * sets properties on (e.g. recordId, name) and the SDK + * makes them available via viewSDK.getUiProps(). * - * 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. + * Key concept: viewSDK.getUiProps() returns { props, subscribe }. The + * props promise resolves with the initial snapshot; the subscribe callback + * fires every time the host updates any prop. Always call the unsubscribe + * function in cleanup so stale handlers don't leak 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; -} +import { useSdk } from '../sdk-context'; export default function ReceiveData() { - const [hostData, setHostData] = useState({}); + const { view, chat } = useSdk(); + const [hostData, setHostData] = useState>({}); const [updateCount, setUpdateCount] = useState(0); + const connected = Object.keys(chat.getHostContext?.() ?? {}).length > 0; useEffect(() => { - const handleData = (e: Event) => { - const detail = (e as CustomEvent).detail ?? {}; - setHostData(detail); + // Fallback for hosts that pass data via URL query params (e.g. the + // mfeReceiveData LWC re-mounts with a new src + // since src is session-binding). URLSearchParams is read-once. + const params = new URLSearchParams(window.location.search); + if ([...params.keys()].length > 0) { + setHostData(Object.fromEntries(params.entries())); + setUpdateCount(c => c + 1); + } + + const uiProps = view.getUiProps?.(); + if (!uiProps) return; + + let unsubscribe: (() => void) | undefined; + + uiProps.props.then(initial => { + if (Object.keys(initial).length > 0) { + setHostData(initial); + setUpdateCount(c => c + 1); + } + }); + + // subscribe() fires on every host-side prop update. Capture the + // returned unsubscribe (when the SDK provides one) for cleanup. + const result = uiProps.subscribe(next => { + setHostData(next); setUpdateCount(c => c + 1); - }; + }) as unknown; + if (typeof result === 'function') unsubscribe = result as () => void; - // 'data' fires whenever the host calls shell.updateData(payload) - bridge.addEventListener('data', handleData); - return () => bridge.removeEventListener('data', handleData); - }, []); + return () => unsubscribe?.(); + }, [view]); const hasData = Object.keys(hostData).length > 0; @@ -43,12 +60,12 @@ export default function ReceiveData() {

    Receive Data

    - Displays data pushed from the Salesforce host via{' '} - shell.updateData(). Interact with the host component to - trigger an update. + Displays UI props pushed from the Salesforce host. The host LWC sets + properties on <lightning-embedding>; the SDK delivers + them via viewSDK.getUiProps().

    - {!bridge.isConnected() && ( + {!connected && (
    Running standalone — no host data will arrive. Embed this app in the mfeReceiveData LWC to see live data. diff --git a/mfe-app/src/recipes/SendEvent.tsx b/mfe-app/src/recipes/SendEvent.tsx index 38c1314..5ab89bb 100644 --- a/mfe-app/src/recipes/SendEvent.tsx +++ b/mfe-app/src/recipes/SendEvent.tsx @@ -2,16 +2,18 @@ * 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). + * The host listens on the embedding 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. + * Key concept: viewSDK.dispatchEvent(name, data) sends the event through + * to the parent LWC. The host catches it as a DOM + * event on the embedding element. The data argument is any serialisable + * key/value bag and lands on event.detail. * * @see ReceiveData — receiving data pushed from the host */ import { useState } from 'react'; -import bridge from '@salesforce/experimental-mfe-bridge'; +import { useSdk } from '../sdk-context'; const ACTIONS = ['approve', 'reject', 'request-docs', 'escalate'] as const; type Action = (typeof ACTIONS)[number]; @@ -22,16 +24,14 @@ interface EventLog { } export default function SendEvent() { + const { view, chat } = useSdk(); const [log, setLog] = useState([]); + const connected = Object.keys(chat.getHostContext?.() ?? {}).length > 0; - function handleAction(action: Action) { - // Dispatch a custom event that bubbles up to the LWC host. + async function handleAction(action: Action) { + // dispatchEvent forwards (name, data) to the host LWC. // The host listens: shell.addEventListener('mfe-action', handler) - bridge.dispatchEvent( - new CustomEvent('mfe-action', { - detail: { action, timestamp: Date.now() }, - }), - ); + await view.dispatchEvent?.('mfe-action', { action, timestamp: Date.now() }); setLog(prev => [ { action, timestamp: new Date().toLocaleTimeString() }, @@ -44,11 +44,11 @@ export default function SendEvent() {

    Send Event

    Dispatches custom events to the Salesforce LWC host via{' '} - bridge.dispatchEvent(). The host receives them on the shell - element. + viewSDK.dispatchEvent(). The host receives them on the + embedding element.

    - {!bridge.isConnected() && ( + {!connected && (
    Running standalone — events are dispatched but no host is listening.
    diff --git a/mfe-app/src/recipes/ThemeTokens.tsx b/mfe-app/src/recipes/ThemeTokens.tsx index e73613c..030ad35 100644 --- a/mfe-app/src/recipes/ThemeTokens.tsx +++ b/mfe-app/src/recipes/ThemeTokens.tsx @@ -1,88 +1,65 @@ /** * 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(). + * Reads the host theme via viewSDK.getTheme() and the broader host context + * via chatSDK.getHostContext(). Applies the theme mode to a CSS class on + * the document root so styles cascade through the whole MFE. * - * 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. + * Key concept: getTheme() returns { mode: 'light' | 'dark' } or null when + * the host hasn't supplied a theme. getHostContext() also exposes locale, + * userAgent, displayMode, and host-provided styles. Re-read on demand — + * the host pushes updates through getUiProps(), so subscribe there if you + * need live theme refreshes. * * @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; -} +import { useSdk } from '../sdk-context'; export default function ThemeTokens() { - const [tokens, setTokens] = useState({}); - const [syncCount, setSyncCount] = useState(0); + const { view, chat } = useSdk(); + const [mode, setMode] = useState(null); + const [hostContext, setHostContext] = useState>({}); + const connected = Object.keys(chat.getHostContext?.() ?? {}).length > 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 theme = view.getTheme?.(); + setMode(theme?.mode ?? null); + setHostContext({ ...(chat.getHostContext?.() ?? {}) }); - const tokenEntries = Object.entries(tokens).slice(0, 20); + if (theme?.mode) { + document.documentElement.dataset.theme = theme.mode; + } + }, [view, chat]); 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. + Reads the host theme via viewSDK.getTheme() and the broader + environment via chatSDK.getHostContext(). Apply the result + to your styles however you like.

    - {!bridge.isConnected() && ( + {!connected && (
    Running standalone — no theme data will arrive from a host.
    )}
    -

    Syncs received: {syncCount}

    -

    Tokens (first 20 of {Object.keys(tokens).length})

    +

    Theme mode

    +

    + {mode ?? '—'} +

    - {tokenEntries.length === 0 ? ( -

    Waiting for theme data…

    +

    Host context

    + {Object.keys(hostContext).length === 0 ? ( +

    No host context yet.

    ) : ( - - - - - - - - - {tokenEntries.map(([prop, value]) => ( - - - - - ))} - -
    PropertyValue
    {prop}{value}
    +
    +                        {JSON.stringify(hostContext, null, 2)}
    +                    
    )}
    diff --git a/mfe-app/src/sdk-context.tsx b/mfe-app/src/sdk-context.tsx new file mode 100644 index 0000000..b98d3e0 --- /dev/null +++ b/mfe-app/src/sdk-context.tsx @@ -0,0 +1,19 @@ +import { createContext, useContext } from 'react'; +import type { ChatSDK, ViewSDK } from '@salesforce/platform-sdk'; + +export interface SdkBundle { + chat: ChatSDK; + view: ViewSDK; +} + +const SdkContext = createContext(null); + +export const SdkProvider = SdkContext.Provider; + +export function useSdk(): SdkBundle { + const sdk = useContext(SdkContext); + if (!sdk) { + throw new Error('useSdk() called outside . Wrap your app at the root.'); + } + return sdk; +} From 5576a0325932ab70f8826f5d7e9135f874473509 Mon Sep 17 00:00:00 2001 From: Charles Watkins <168678941+charlesw-salesforce@users.noreply.github.com> Date: Thu, 9 Jul 2026 12:39:14 -0700 Subject: [PATCH 04/10] build(deps): bump @salesforce/platform-sdk, ui-bundle, and vite-plugin-ui-bundle to ^10.24.0 (#40) --- .../uiBundles/reactRecipes/package-lock.json | 728 ++++++++++++++++-- .../uiBundles/reactRecipes/package.json | 10 +- .../uiBundles/reactRecipes/src/api/account.ts | 10 +- .../reactRecipes/src/api/accounts.ts | 6 +- .../reactRecipes/src/api/contacts.ts | 6 +- .../src/api/graphql-operations-types.ts | 1 + .../error-handling/GraphqlErrors.test.tsx | 6 +- .../recipes/error-handling/GraphqlErrors.tsx | 4 +- .../error-handling/LoadingErrorEmpty.test.tsx | 4 +- .../error-handling/LoadingErrorEmpty.tsx | 4 +- .../recipes/hello/BindingAccountName.test.tsx | 6 +- .../src/recipes/hello/BindingAccountName.tsx | 4 +- .../src/recipes/hello/ChildToParent.test.tsx | 6 +- .../src/recipes/hello/ChildToParent.tsx | 4 +- .../recipes/hello/ConditionalStatus.test.tsx | 6 +- .../src/recipes/hello/ConditionalStatus.tsx | 4 +- .../src/recipes/hello/LifecycleFetch.test.tsx | 6 +- .../src/recipes/hello/LifecycleFetch.tsx | 4 +- .../src/recipes/hello/ListOfAccounts.test.tsx | 8 +- .../src/recipes/hello/ListOfAccounts.tsx | 4 +- .../src/recipes/hello/ParentToChild.test.tsx | 6 +- .../src/recipes/hello/ParentToChild.tsx | 4 +- .../recipes/hello/StateManagement.test.tsx | 6 +- .../src/recipes/hello/StateManagement.tsx | 4 +- .../DashboardAliasedQueries.test.tsx | 6 +- .../integration/DashboardAliasedQueries.tsx | 4 +- .../SearchableAccountList.test.tsx | 6 +- .../integration/SearchableAccountList.tsx | 4 +- .../recipes/modify-data/CreateRecord.test.tsx | 8 +- .../src/recipes/modify-data/CreateRecord.tsx | 8 +- .../recipes/modify-data/DeleteRecord.test.tsx | 8 +- .../src/recipes/modify-data/DeleteRecord.tsx | 10 +- .../QueryMutationTogether.test.tsx | 8 +- .../modify-data/QueryMutationTogether.tsx | 12 +- .../modify-data/ServerErrorHandling.test.tsx | 6 +- .../modify-data/ServerErrorHandling.tsx | 8 +- .../recipes/modify-data/UpdateRecord.test.tsx | 8 +- .../src/recipes/modify-data/UpdateRecord.tsx | 12 +- .../AliasedMultiObjectQuery.test.tsx | 6 +- .../read-data/AliasedMultiObjectQuery.tsx | 4 +- .../recipes/read-data/FilteredList.test.tsx | 6 +- .../src/recipes/read-data/FilteredList.tsx | 4 +- .../read-data/ImperativeRefetch.test.tsx | 6 +- .../recipes/read-data/ImperativeRefetch.tsx | 4 +- .../recipes/read-data/ListOfRecords.test.tsx | 6 +- .../src/recipes/read-data/ListOfRecords.tsx | 4 +- .../recipes/read-data/PaginatedList.test.tsx | 6 +- .../src/recipes/read-data/PaginatedList.tsx | 4 +- .../recipes/read-data/RelatedRecords.test.tsx | 6 +- .../src/recipes/read-data/RelatedRecords.tsx | 4 +- .../recipes/read-data/SingleRecord.test.tsx | 6 +- .../src/recipes/read-data/SingleRecord.tsx | 4 +- .../recipes/read-data/SortedResults.test.tsx | 6 +- .../src/recipes/read-data/SortedResults.tsx | 4 +- .../src/recipes/routing/NestedRoutes.test.tsx | 6 +- .../src/recipes/routing/NestedRoutes.tsx | 4 +- .../recipes/routing/RouteParameters.test.tsx | 8 +- .../src/recipes/routing/RouteParameters.tsx | 6 +- .../recipes/salesforce-apis/ApexRest.test.tsx | 4 +- .../src/recipes/salesforce-apis/ApexRest.tsx | 2 +- .../salesforce-apis/ConnectApi.test.tsx | 4 +- .../recipes/salesforce-apis/ConnectApi.tsx | 2 +- .../DisplayCurrentUser.test.tsx | 4 +- .../salesforce-apis/DisplayCurrentUser.tsx | 2 +- .../salesforce-apis/UiApiRest.test.tsx | 4 +- .../src/recipes/salesforce-apis/UiApiRest.tsx | 2 +- .../uiBundles/reactRecipes/tsconfig.json | 1 - 67 files changed, 857 insertions(+), 231 deletions(-) diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/package-lock.json b/force-app/main/react-recipes/uiBundles/reactRecipes/package-lock.json index 6426149..76d9853 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/package-lock.json +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/package-lock.json @@ -10,8 +10,8 @@ "dependencies": { "@salesforce-ux/design-system": "2.29.0", "@salesforce/design-system-react": "^0.10.65", - "@salesforce/sdk-data": "^1.134.4", - "@salesforce/ui-bundle": "^1.134.4", + "@salesforce/platform-sdk": "^10.24.0", + "@salesforce/ui-bundle": "^10.24.0", "@tailwindcss/vite": "^4.1.17", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -21,7 +21,7 @@ "react": "^19.2.0", "react-day-picker": "^9.14.0", "react-dom": "^19.2.0", - "react-router": "^7.15.1", + "react-router": "^7.10.1", "shadcn": "^3.8.5", "sonner": "^2.0.7", "tailwind-merge": "^3.5.0", @@ -36,7 +36,7 @@ "@graphql-eslint/eslint-plugin": "^4.1.0", "@graphql-tools/utils": "^11.0.0", "@playwright/test": "^1.49.0", - "@salesforce/vite-plugin-ui-bundle": "^1.134.4", + "@salesforce/vite-plugin-ui-bundle": "^10.24.0", "@shikijs/twoslash": "^4.0.2", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.6.3", @@ -61,7 +61,7 @@ "shiki": "^4.0.2", "typescript": "~5.9.3", "typescript-eslint": "^8.46.4", - "vite": "^7.3.5", + "vite": "^7.2.4", "vite-plugin-graphql-codegen": "^3.6.3", "vitest": "^4.0.17", "vitest-axe": "^1.0.0-pre.5" @@ -604,39 +604,283 @@ "node": ">=18" } }, + "node_modules/@conduit-client/command-base": { + "version": "3.19.6", + "resolved": "https://registry.npmjs.org/@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://registry.npmjs.org/@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://registry.npmjs.org/@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://registry.npmjs.org/@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.17.0", - "resolved": "https://registry.npmjs.org/@conduit-client/jwt-manager/-/jwt-manager-3.17.0.tgz", - "integrity": "sha512-u+NgdadxOqfKy7ak7z0zCrcAL4A66mOIuekob6pjogswi8cgebvtvFRO4Mag53n6pSCmRZ4DNlXIe5eylpXo3g==", + "version": "3.19.6", + "resolved": "https://registry.npmjs.org/@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://registry.npmjs.org/@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/onestore-graphql-parser/node_modules/graphql": { + "version": "16.11.0", + "resolved": "https://registry.npmjs.org/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/@conduit-client/service-aura-network": { + "version": "3.19.6", + "resolved": "https://registry.npmjs.org/@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://registry.npmjs.org/@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://registry.npmjs.org/@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://registry.npmjs.org/@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.17.0", - "resolved": "https://registry.npmjs.org/@conduit-client/service-fetch-network/-/service-fetch-network-3.17.0.tgz", - "integrity": "sha512-K+KuFWU2N4kF+k/fsS4PSOM0eGdsTGfEtAuoG+/cuRPaQz2KkW33Gyk/KZGQyF+7ikBh0kGATXX1DDF+8XFopQ==", + "version": "3.19.6", + "resolved": "https://registry.npmjs.org/@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://registry.npmjs.org/@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://registry.npmjs.org/@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-noop/node_modules/@opentelemetry/api": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.7.0.tgz", + "integrity": "sha512-AdY5wvN0P2vXBi3b29hxZgSFvdhdxPB9+f0B6s//P9Q8nibRWeA3cHm8UmLpio9ABigkVHJ5NMPk+Mz8VCCyrw==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@conduit-client/service-instrumentation-o11y": { + "version": "3.19.6", + "resolved": "https://registry.npmjs.org/@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/@opentelemetry/api": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.7.0.tgz", + "integrity": "sha512-AdY5wvN0P2vXBi3b29hxZgSFvdhdxPB9+f0B6s//P9Q8nibRWeA3cHm8UmLpio9ABigkVHJ5NMPk+Mz8VCCyrw==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@conduit-client/service-instrumentation-o11y/node_modules/o11y": { + "version": "252.7.0", + "resolved": "https://registry.npmjs.org/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://registry.npmjs.org/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://registry.npmjs.org/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://registry.npmjs.org/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://registry.npmjs.org/@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-instrumentation-types/node_modules/@opentelemetry/api": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.7.0.tgz", + "integrity": "sha512-AdY5wvN0P2vXBi3b29hxZgSFvdhdxPB9+f0B6s//P9Q8nibRWeA3cHm8UmLpio9ABigkVHJ5NMPk+Mz8VCCyrw==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@conduit-client/service-instrumentation/node_modules/@opentelemetry/api": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.7.0.tgz", + "integrity": "sha512-AdY5wvN0P2vXBi3b29hxZgSFvdhdxPB9+f0B6s//P9Q8nibRWeA3cHm8UmLpio9ABigkVHJ5NMPk+Mz8VCCyrw==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@conduit-client/service-pubsub": { + "version": "3.19.6", + "resolved": "https://registry.npmjs.org/@conduit-client/service-pubsub/-/service-pubsub-3.19.6.tgz", + "integrity": "sha512-HgWWiXMX43jorZNtqEHx/8cxRImcga6ycjCS6NRPnYStsndIBZIR53GnKcXbPvvaO/v5BhA3WWiWwSYe38OF2A==", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { - "@conduit-client/jwt-manager": "3.17.0", - "@conduit-client/service-retry": "3.17.0", - "@conduit-client/utils": "3.17.0" + "@conduit-client/utils": "3.19.6" } }, "node_modules/@conduit-client/service-retry": { - "version": "3.17.0", - "resolved": "https://registry.npmjs.org/@conduit-client/service-retry/-/service-retry-3.17.0.tgz", - "integrity": "sha512-IrjXZUltgx7P47/cniQfvQF6H2jMlsOCOUcNGm4B1fRQpi8dyv2/Rh7VQUbIRSfYsIIJA1x/yf/BdY8Mo4Q8Jw==", + "version": "3.19.6", + "resolved": "https://registry.npmjs.org/@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.17.0" + "@conduit-client/utils": "3.19.6" } }, "node_modules/@conduit-client/utils": { - "version": "3.17.0", - "resolved": "https://registry.npmjs.org/@conduit-client/utils/-/utils-3.17.0.tgz", - "integrity": "sha512-jeq1b3I/Bxe9XHQ2CjBZcc+mkY1TxUtjuowmF8iw9A6PZYG5Sl2TMxyCtbZySHxbHMdWWc3JDJuVwc/m7do0eg==", + "version": "3.19.6", + "resolved": "https://registry.npmjs.org/@conduit-client/utils/-/utils-3.19.6.tgz", + "integrity": "sha512-sEE8RIz1+LxPJJVkBsSATgQBEn91pKdiEahUqisnodmnct+uLbI+bi/k4NPXoNMSIZM7W2VgEWFQDHF2GXZ28w==", "license": "SEE LICENSE IN LICENSE.txt" }, "node_modules/@csstools/color-helpers": { @@ -3216,6 +3460,18 @@ "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", "license": "MIT" }, + "node_modules/@opentelemetry/api": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", + "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "peer": true, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@pinojs/redact": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", @@ -3245,6 +3501,69 @@ "dev": true, "license": "MIT" }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@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://registry.npmjs.org/@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://registry.npmjs.org/@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://registry.npmjs.org/@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://registry.npmjs.org/@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://registry.npmjs.org/@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://registry.npmjs.org/@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://registry.npmjs.org/@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://registry.npmjs.org/@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://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", + "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", + "license": "BSD-3-Clause" + }, "node_modules/@radix-ui/number": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", @@ -3762,6 +4081,16 @@ } } }, + "node_modules/@radix-ui/react-icons": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.2.tgz", + "integrity": "sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "react": "^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/@radix-ui/react-id": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", @@ -5221,6 +5550,12 @@ "react-dom": "^15.4.1 || ^16.0.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/@salesforce/jsonrpc": { + "version": "10.24.0", + "resolved": "https://registry.npmjs.org/@salesforce/jsonrpc/-/jsonrpc-10.24.0.tgz", + "integrity": "sha512-kqN/2XOMnjxiBa59cAm6S6RTHNw6H0YRC/5wCp/2xOhFeZiT25HpPB7dKLMF4OsnItSmO8lvvkl0UUvEPkDCUw==", + "license": "SEE LICENSE IN LICENSE.txt" + }, "node_modules/@salesforce/kit": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/@salesforce/kit/-/kit-3.2.6.tgz", @@ -5230,21 +5565,27 @@ "@salesforce/ts-types": "^2.0.12" } }, - "node_modules/@salesforce/sdk-core": { - "version": "1.134.4", - "resolved": "https://registry.npmjs.org/@salesforce/sdk-core/-/sdk-core-1.134.4.tgz", - "integrity": "sha512-ZqcK4ey6TKwNTxnLM1rNifNW8achYKTqzNhwE6d+hStufhgF/lnlvoYxxrBsnS1+EVD3EdOEBWrNNcbA1PyluA==", - "license": "SEE LICENSE IN LICENSE.txt" - }, - "node_modules/@salesforce/sdk-data": { - "version": "1.134.4", - "resolved": "https://registry.npmjs.org/@salesforce/sdk-data/-/sdk-data-1.134.4.tgz", - "integrity": "sha512-7ifc4laGHmTQkw48YRBicd8lgIPrMBhN9FiylWLFoJ8IOMQWvckZfXhYCHV+f8QgLTjIdQP5sq9XdJHwPnBX9A==", + "node_modules/@salesforce/platform-sdk": { + "version": "10.24.0", + "resolved": "https://registry.npmjs.org/@salesforce/platform-sdk/-/platform-sdk-10.24.0.tgz", + "integrity": "sha512-5Jyt1TgNvJGU/p0XAPWU8KhlOpzldPYD0XW+ynWd68fve0OOJVEtpb93+g/9AoE8w4vSx2Eru0UwNdg8r0ZyWw==", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { - "@conduit-client/service-fetch-network": "3.17.0", - "@conduit-client/utils": "3.17.0", - "@salesforce/sdk-core": "^1.134.4" + "@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.24.0", + "@sf-embedding/sf-embedding-bridge": "2.2.1-rc.4" + }, + "peerDependencies": { + "o11y": ">=260.0.0", + "o11y_schema": ">=264.85.0" } }, "node_modules/@salesforce/ts-types": { @@ -5257,13 +5598,13 @@ } }, "node_modules/@salesforce/ui-bundle": { - "version": "1.134.4", - "resolved": "https://registry.npmjs.org/@salesforce/ui-bundle/-/ui-bundle-1.134.4.tgz", - "integrity": "sha512-+oMQukfnXn9FJErQGjlMqahRbTSCm39es/xXcf86zdUhXJ7mJvS8eg6K0aqc9TQ+mTa16vsO0k7Y9WpjrhF0sA==", + "version": "10.24.0", + "resolved": "https://registry.npmjs.org/@salesforce/ui-bundle/-/ui-bundle-10.24.0.tgz", + "integrity": "sha512-Q3yWid7nv2VFoxlfOTDaAqF9vEfAcqfEJpOA3YxL4Ccm3CSG7Gp7eUhCtT99qXM/QcqnSkERdrPeZ41XdQRL1g==", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@salesforce/core": "^8.23.4", - "@salesforce/sdk-data": "^1.134.4", + "@salesforce/platform-sdk": "^10.24.0", "micromatch": "^4.0.8", "path-to-regexp": "^8.3.0" }, @@ -5271,16 +5612,50 @@ "node": ">=20.0.0" } }, + "node_modules/@salesforce/ui-design-mode": { + "version": "10.24.0", + "resolved": "https://registry.npmjs.org/@salesforce/ui-design-mode/-/ui-design-mode-10.24.0.tgz", + "integrity": "sha512-8XILHoc0+cLQhEbFrxS2L4Fg9VtmF1QYtGSG7/tUHxyfcP96aTkue7nzJiLVE+2V6XWHO/GPIOVh7GHDPNJZUQ==", + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.3", + "@radix-ui/react-icons": "1.3.2", + "@radix-ui/react-toggle-group": "1.1.11", + "culori": "^4.0.1", + "downshift": "9.3.2", + "lucide-react": "0.575.0", + "react-beautiful-color": "2.1.0", + "use-debounce": "^10.0.4" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": "^19.2.0", + "react-dom": "^19.2.0" + } + }, + "node_modules/@salesforce/ui-design-mode/node_modules/lucide-react": { + "version": "0.575.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.575.0.tgz", + "integrity": "sha512-VuXgKZrk0uiDlWjGGXmKV6MSk9Yy4l10qgVvzGn2AWBx1Ylt0iBexKOAoA6I7JO3m+M9oeovJd3yYENfkUbOeg==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@salesforce/vite-plugin-ui-bundle": { - "version": "1.134.4", - "resolved": "https://registry.npmjs.org/@salesforce/vite-plugin-ui-bundle/-/vite-plugin-ui-bundle-1.134.4.tgz", - "integrity": "sha512-IDvsrveejQx2Wr7f+265P60KgQ2QVMEFvzrvDKjyOnghqYUN6bjHygrerZRcYbr+FzF6RNhoe6hLLHGsFJAnAg==", + "version": "10.24.0", + "resolved": "https://registry.npmjs.org/@salesforce/vite-plugin-ui-bundle/-/vite-plugin-ui-bundle-10.24.0.tgz", + "integrity": "sha512-RbgVFDvWkPOjOtnAGkihMsHtJsGkmud+NNUWAtnRY10oEzQ21vMofBmplWFUtgF49Trsf1GHbn/zVCnC4PdqWA==", "dev": true, "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@babel/core": "^7.28.4", - "@babel/helper-plugin-utils": "^7.28.3", - "@salesforce/ui-bundle": "^1.134.4" + "@salesforce/ui-bundle": "^10.24.0", + "@salesforce/ui-design-mode": "^10.24.0" }, "engines": { "node": ">=20.0.0" @@ -5295,6 +5670,21 @@ "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", "license": "MIT" }, + "node_modules/@sf-embedding/sf-embedding-bridge": { + "version": "2.2.1-rc.4", + "resolved": "https://registry.npmjs.org/@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://registry.npmjs.org/@salesforce/jsonrpc/-/jsonrpc-9.21.0.tgz", + "integrity": "sha512-EpUBVDpBc6bKXIVmhvdpw54r3U9oi+KLbatax9/kJyes04yxSRvchwPCHoCQkYnoE3vojCbZ2OTubg8RzAL0xQ==", + "license": "SEE LICENSE IN LICENSE.txt" + }, "node_modules/@shikijs/core": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.0.2.tgz", @@ -6019,7 +6409,6 @@ "version": "24.12.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", - "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~7.16.0" @@ -8006,6 +8395,13 @@ "dev": true, "license": "MIT" }, + "node_modules/compute-scroll-into-view": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz", + "integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==", + "dev": true, + "license": "MIT" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -8228,6 +8624,16 @@ "integrity": "sha512-UdtziYp5HuTz7e5j8Nvq+a/3HQo+2/aJZ9xntNTpmRRIg/3YYqDVgiS9fvAhtNbnyfbv2ZBe0bqCHqzhE7FqWQ==", "license": "MIT" }, + "node_modules/culori": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/culori/-/culori-4.0.2.tgz", + "integrity": "sha512-1+BhOB8ahCn4O0cep0Sh2l9KCOfOdY+BXJnKMHFFzDEouSr/el18QwXEMRlOj9UY5nCeA8UN3a/82rUWRBeyBw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -8631,6 +9037,30 @@ "url": "https://dotenvx.com" } }, + "node_modules/downshift": { + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/downshift/-/downshift-9.3.2.tgz", + "integrity": "sha512-5VD0WZLQDhipWiDU+K5ili3VDhGrXwlvOlSaSG1Cb0eS4XpssxVuoD09JNgju+bAzxB2Wvlwx+FwTE/FNdrqow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "compute-scroll-into-view": "^3.1.1", + "prop-types": "^15.8.1", + "react-is": "^18.2.0", + "tslib": "^2.8.1" + }, + "peerDependencies": { + "react": ">=16.12.0" + } + }, + "node_modules/downshift/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -12623,6 +13053,12 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -13313,6 +13749,32 @@ "dev": true, "license": "MIT" }, + "node_modules/o11y": { + "version": "264.20.0", + "resolved": "https://registry.npmjs.org/o11y/-/o11y-264.20.0.tgz", + "integrity": "sha512-QmwrEv2CYNzEIOnArYP1522vbNuB+KTAWu4K+8174bZ5ua2aSFMQW0Q7MLLcl6QrekMBTRt/HmL4sNL5pnr5xg==", + "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.90.0", + "resolved": "https://registry.npmjs.org/o11y_schema/-/o11y_schema-264.90.0.tgz", + "integrity": "sha512-b4Z87+C+RIeqqeXW/Gee3cPeBu0xXDygYZXZRfJivJU2qvTpt4btJDUXLqxxC8A7KPQlYIrs5yDxocCqEX5+WQ==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/o11y/node_modules/o11y_schema": { + "version": "260.5.0", + "resolved": "https://registry.npmjs.org/o11y_schema/-/o11y_schema-260.5.0.tgz", + "integrity": "sha512-0qrtqE394CODoPovUBdQQ2efHls6Z2xP6jvLitEGKSEj1KcCyeBLx2dHwTUtv66zwZTKjSp27QLFF4PZhengEQ==", + "license": "BSD-3-Clause", + "peer": true + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -14214,6 +14676,110 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.8.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.5.tgz", + "integrity": "sha512-zxcTTCedNGJM4R8sj/Cq/F0W/c4iE0afWBcBwMTRtw4WHYP9TWkYjdiH3npPRUYsXQCPR0hTU9yjovOu+E6EQA==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-tailwindcss": { + "version": "0.6.14", + "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.14.tgz", + "integrity": "sha512-pi2e/+ZygeIqntN+vC573BcW5Cve8zUB0SSAGxqpB4f96boZF4M3phPVoOFCeypwkpRYdi7+jQ5YJJUwrkGUAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.21.3" + }, + "peerDependencies": { + "@ianvs/prettier-plugin-sort-imports": "*", + "@prettier/plugin-hermes": "*", + "@prettier/plugin-oxc": "*", + "@prettier/plugin-pug": "*", + "@shopify/prettier-plugin-liquid": "*", + "@trivago/prettier-plugin-sort-imports": "*", + "@zackad/prettier-plugin-twig": "*", + "prettier": "^3.0", + "prettier-plugin-astro": "*", + "prettier-plugin-css-order": "*", + "prettier-plugin-import-sort": "*", + "prettier-plugin-jsdoc": "*", + "prettier-plugin-marko": "*", + "prettier-plugin-multiline-arrays": "*", + "prettier-plugin-organize-attributes": "*", + "prettier-plugin-organize-imports": "*", + "prettier-plugin-sort-imports": "*", + "prettier-plugin-style-order": "*", + "prettier-plugin-svelte": "*" + }, + "peerDependenciesMeta": { + "@ianvs/prettier-plugin-sort-imports": { + "optional": true + }, + "@prettier/plugin-hermes": { + "optional": true + }, + "@prettier/plugin-oxc": { + "optional": true + }, + "@prettier/plugin-pug": { + "optional": true + }, + "@shopify/prettier-plugin-liquid": { + "optional": true + }, + "@trivago/prettier-plugin-sort-imports": { + "optional": true + }, + "@zackad/prettier-plugin-twig": { + "optional": true + }, + "prettier-plugin-astro": { + "optional": true + }, + "prettier-plugin-css-order": { + "optional": true + }, + "prettier-plugin-import-sort": { + "optional": true + }, + "prettier-plugin-jsdoc": { + "optional": true + }, + "prettier-plugin-marko": { + "optional": true + }, + "prettier-plugin-multiline-arrays": { + "optional": true + }, + "prettier-plugin-organize-attributes": { + "optional": true + }, + "prettier-plugin-organize-imports": { + "optional": true + }, + "prettier-plugin-sort-imports": { + "optional": true + }, + "prettier-plugin-style-order": { + "optional": true + }, + "prettier-plugin-svelte": { + "optional": true + } + } + }, "node_modules/pretty-format": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", @@ -14355,6 +14921,31 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/protobufjs": { + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/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/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -14566,6 +15157,22 @@ "node": ">=0.10.0" } }, + "node_modules/react-beautiful-color": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/react-beautiful-color/-/react-beautiful-color-2.1.0.tgz", + "integrity": "sha512-6pjk4U9VQkxQ2StqoptSmUBqbXKOwSWuU/A0dOHn3j6U7hMs4ethdpNdU/whsFbdXffU1b0eYg5SZ9WD7osl7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "clsx": "^2.1.1", + "prettier-plugin-tailwindcss": "^0.6.8", + "tailwind-merge": "^3.3.1" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, "node_modules/react-contenteditable": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/react-contenteditable/-/react-contenteditable-3.3.7.tgz", @@ -14712,9 +15319,9 @@ "license": "MIT" }, "node_modules/react-router": { - "version": "7.15.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.15.1.tgz", - "integrity": "sha512-R8rl9HhgikFYoPJymnUtPXWbnDb3oget6lQnfIoupbt61aT9aOhRkDsY2XRhZRyX1Z/8a5sL74fXmFNm3NRK5A==", + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.0.tgz", + "integrity": "sha512-m/xR9N4LQLmAS0ZhkY2nkPA1N7gQ5TUVa5n8TgANuDTARbn1gt+zLPXEm7W0XDTbrQ2AJSJKhoa6yx1D8BcpxQ==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", @@ -16981,7 +17588,6 @@ "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "devOptional": true, "license": "MIT" }, "node_modules/unicorn-magic": { @@ -17206,6 +17812,19 @@ } } }, + "node_modules/use-debounce": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-10.1.1.tgz", + "integrity": "sha512-kvds8BHR2k28cFsxW8k3nc/tRga2rs1RHYCqmmGqb90MEeE++oALwzh2COiuBLO1/QXiOuShXoSN2ZpWnMmvuQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16.0.0" + }, + "peerDependencies": { + "react": "*" + } + }, "node_modules/use-sidecar": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", @@ -17292,9 +17911,9 @@ } }, "node_modules/vite": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.5.tgz", - "integrity": "sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==", + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", + "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", "license": "MIT", "dependencies": { "esbuild": "^0.27.0", @@ -18063,6 +18682,13 @@ "node": ">= 8" } }, + "node_modules/web-vitals": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-5.3.0.tgz", + "integrity": "sha512-q6LWsLatGYZp5VGBIOvbTj6JBV2nOmC8KvWztXBmwJcfFAzhwKwbOxhUH306XY3CcaZDUlSmSuNPBsCn0bFu+g==", + "license": "Apache-2.0", + "peer": true + }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/package.json b/force-app/main/react-recipes/uiBundles/reactRecipes/package.json index 3f89cff..22d8fe3 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/package.json +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/package.json @@ -19,8 +19,8 @@ "dependencies": { "@salesforce-ux/design-system": "2.29.0", "@salesforce/design-system-react": "^0.10.65", - "@salesforce/sdk-data": "^1.134.4", - "@salesforce/ui-bundle": "^1.134.4", + "@salesforce/platform-sdk": "^10.24.0", + "@salesforce/ui-bundle": "^10.24.0", "@tailwindcss/vite": "^4.1.17", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -30,7 +30,7 @@ "react": "^19.2.0", "react-day-picker": "^9.14.0", "react-dom": "^19.2.0", - "react-router": "^7.15.1", + "react-router": "^7.10.1", "shadcn": "^3.8.5", "sonner": "^2.0.7", "tailwind-merge": "^3.5.0", @@ -50,7 +50,7 @@ "@graphql-eslint/eslint-plugin": "^4.1.0", "@graphql-tools/utils": "^11.0.0", "@playwright/test": "^1.49.0", - "@salesforce/vite-plugin-ui-bundle": "^1.134.4", + "@salesforce/vite-plugin-ui-bundle": "^10.24.0", "@shikijs/twoslash": "^4.0.2", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.6.3", @@ -75,7 +75,7 @@ "shiki": "^4.0.2", "typescript": "~5.9.3", "typescript-eslint": "^8.46.4", - "vite": "^7.3.5", + "vite": "^7.2.4", "vite-plugin-graphql-codegen": "^3.6.3", "vitest": "^4.0.17", "vitest-axe": "^1.0.0-pre.5" diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/api/account.ts b/force-app/main/react-recipes/uiBundles/reactRecipes/src/api/account.ts index a9b5916..56eeea3 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/api/account.ts +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/api/account.ts @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react'; -import { createDataSDK, gql } from '@salesforce/sdk-data'; +import { createDataSDK, gql } from '@salesforce/platform-sdk'; import type { GetFirstAccountQuery, GetAccountsQuery, @@ -65,13 +65,13 @@ export function useFirstAccount(): { export async function getFirstAccount(): Promise { const sdk = await createDataSDK(); - const result = await sdk.graphql?.({ query: GET_FIRST_ACCOUNT }); + const result = await sdk.graphql?.query({ query: GET_FIRST_ACCOUNT }); if (result?.errors?.length) { throw new Error(result.errors.map(e => e.message).join('; ')); } - return result?.data.uiapi?.query?.Account?.edges?.[0]?.node ?? undefined; + return result?.data?.uiapi?.query?.Account?.edges?.[0]?.node ?? undefined; } const GET_ACCOUNTS = gql` @@ -106,13 +106,13 @@ export type Account = NonNullable; export async function getAccounts(): Promise { const sdk = await createDataSDK(); - const result = await sdk.graphql?.({ query: GET_ACCOUNTS }); + const result = await sdk.graphql?.query({ query: GET_ACCOUNTS }); if (result?.errors?.length) { throw new Error(result.errors.map(e => e.message).join('; ')); } - return (result?.data.uiapi?.query?.Account?.edges ?? []) + return (result?.data?.uiapi?.query?.Account?.edges ?? []) .map(edge => edge?.node) .filter((node): node is Account => node != null); } diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/api/accounts.ts b/force-app/main/react-recipes/uiBundles/reactRecipes/src/api/accounts.ts index 7e4434a..62a2bd7 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/api/accounts.ts +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/api/accounts.ts @@ -1,4 +1,4 @@ -import { createDataSDK, gql } from '@salesforce/sdk-data'; +import { createDataSDK, gql } from '@salesforce/platform-sdk'; import { GetAccountsWithContactsQuery } from './graphql-operations-types'; const GET_ACCOUNTS_WITH_CONTACTS = gql` @@ -56,7 +56,7 @@ export async function getAccountsWithContacts(): Promise< AccountWithContacts[] > { const sdk = await createDataSDK(); - const result = await sdk.graphql?.({ + const result = await sdk.graphql?.query({ query: GET_ACCOUNTS_WITH_CONTACTS, }); @@ -64,7 +64,7 @@ export async function getAccountsWithContacts(): Promise< throw new Error(result.errors.map(e => e.message).join('; ')); } - return (result?.data.uiapi?.query?.Account?.edges ?? []) + return (result?.data?.uiapi?.query?.Account?.edges ?? []) .map(edge => edge?.node) .filter((node): node is AccountWithContacts => node != null); } diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/api/contacts.ts b/force-app/main/react-recipes/uiBundles/reactRecipes/src/api/contacts.ts index 6f7ec44..062be62 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/api/contacts.ts +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/api/contacts.ts @@ -1,4 +1,4 @@ -import { createDataSDK, gql } from '@salesforce/sdk-data'; +import { createDataSDK, gql } from '@salesforce/platform-sdk'; import { GetContactsQuery } from './graphql-operations-types'; const GET_CONTACTS = gql` @@ -45,13 +45,13 @@ export type Contact = ContactNode; export async function getContacts(): Promise<(ContactNode | undefined)[]> { const sdk = await createDataSDK(); - const result = await sdk.graphql?.({ query: GET_CONTACTS }); + const result = await sdk.graphql?.query({ query: GET_CONTACTS }); if (result?.errors?.length) { throw new Error(result.errors.map(e => e.message).join('; ')); } - const connection = result?.data.uiapi?.query?.Contact; + const connection = result?.data?.uiapi?.query?.Contact; return (connection?.edges ?? []) .map(edge => edge?.node) diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/api/graphql-operations-types.ts b/force-app/main/react-recipes/uiBundles/reactRecipes/src/api/graphql-operations-types.ts index 253ff8d..461415b 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/api/graphql-operations-types.ts +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/api/graphql-operations-types.ts @@ -21,6 +21,7 @@ export type Scalars = { EncryptedString: { input: string; output: string; } /** Can be set to an ID or a Reference to the result of another mutation operation. */ IdOrRef: { input: string; output: string; } + JSON: { input: string; output: string; } Latitude: { input: number | string; output: number; } /** A 64-bit signed integer */ Long: { input: number; output: number; } diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/error-handling/GraphqlErrors.test.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/error-handling/GraphqlErrors.test.tsx index 7f8971d..da7c9ee 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/error-handling/GraphqlErrors.test.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/error-handling/GraphqlErrors.test.tsx @@ -9,11 +9,11 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { type Mock } from 'vitest'; -import { createDataSDK } from '@salesforce/sdk-data'; +import { createDataSDK } from '@salesforce/platform-sdk'; import { axe } from 'vitest-axe'; import GraphqlErrors from './GraphqlErrors'; -vi.mock('@salesforce/sdk-data', () => ({ +vi.mock('@salesforce/platform-sdk', () => ({ createDataSDK: vi.fn(), gql: (strings: TemplateStringsArray) => strings.join(''), })); @@ -22,7 +22,7 @@ describe('GraphqlErrors', () => { const mockGraphql = vi.fn(); beforeEach(() => { - (createDataSDK as Mock).mockResolvedValue({ graphql: mockGraphql }); + (createDataSDK as Mock).mockResolvedValue({ graphql: { query: mockGraphql, mutate: mockGraphql } }); }); afterEach(() => { diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/error-handling/GraphqlErrors.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/error-handling/GraphqlErrors.tsx index 90c390f..2bd3fb9 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/error-handling/GraphqlErrors.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/error-handling/GraphqlErrors.tsx @@ -12,7 +12,7 @@ * @see ErrorBoundaryRecipe — catching render-time JavaScript errors */ import { useState } from 'react'; -import { createDataSDK, gql } from '@salesforce/sdk-data'; +import { createDataSDK, gql } from '@salesforce/platform-sdk'; import { Button } from '@/components/ui/button'; // This query intentionally asks for a field that doesn't exist on Account. @@ -71,7 +71,7 @@ export default function GraphqlErrors() { try { const sdk = await createDataSDK(); - const result = await sdk.graphql?.({ query }); + const result = await sdk.graphql?.query({ query }); // Layer 1: result.errors[] — query-level errors (bad fields, auth, etc.) if (result?.errors?.length) { diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/error-handling/LoadingErrorEmpty.test.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/error-handling/LoadingErrorEmpty.test.tsx index 6ea2c0b..a2f551e 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/error-handling/LoadingErrorEmpty.test.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/error-handling/LoadingErrorEmpty.test.tsx @@ -13,8 +13,8 @@ import LoadingErrorEmpty from './LoadingErrorEmpty'; const mockGraphql = vi.fn(); -vi.mock('@salesforce/sdk-data', () => ({ - createDataSDK: vi.fn(() => Promise.resolve({ graphql: mockGraphql })), +vi.mock('@salesforce/platform-sdk', () => ({ + createDataSDK: vi.fn(() => Promise.resolve({ graphql: { query: mockGraphql, mutate: mockGraphql } })), gql: (strings: TemplateStringsArray) => strings.join(''), })); diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/error-handling/LoadingErrorEmpty.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/error-handling/LoadingErrorEmpty.tsx index c63da77..51a9ab4 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/error-handling/LoadingErrorEmpty.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/error-handling/LoadingErrorEmpty.tsx @@ -13,7 +13,7 @@ * @see GraphqlErrors — inspecting GraphQL error objects */ import { useEffect, useState } from 'react'; -import { createDataSDK, gql } from '@salesforce/sdk-data'; +import { createDataSDK, gql } from '@salesforce/platform-sdk'; import Skeleton from '@/components/recipe/Skeleton'; import { Button } from '@/components/ui/button'; import { Avatar, AvatarImage } from '@/components/ui/avatar'; @@ -89,7 +89,7 @@ export default function LoadingErrorEmpty() { function fetchContacts() { const run = async () => { const sdk = await createDataSDK(); - const result = await sdk.graphql?.({ query: QUERY }); + const result = await sdk.graphql?.query({ query: QUERY }); if (result?.errors?.length) { throw new Error( diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/BindingAccountName.test.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/BindingAccountName.test.tsx index 6b1fe46..bdc5788 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/BindingAccountName.test.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/BindingAccountName.test.tsx @@ -6,11 +6,11 @@ */ import { render, screen } from '@testing-library/react'; import type { Mock } from 'vitest'; -import { createDataSDK } from '@salesforce/sdk-data'; +import { createDataSDK } from '@salesforce/platform-sdk'; import { axe } from 'vitest-axe'; import BindingAccountName from './BindingAccountName'; -vi.mock('@salesforce/sdk-data', () => ({ +vi.mock('@salesforce/platform-sdk', () => ({ createDataSDK: vi.fn(), gql: (strings: TemplateStringsArray) => strings.join(''), })); @@ -32,7 +32,7 @@ describe('BindingAccountName', () => { const mockGraphql = vi.fn(); beforeEach(() => { - (createDataSDK as Mock).mockResolvedValue({ graphql: mockGraphql }); + (createDataSDK as Mock).mockResolvedValue({ graphql: { query: mockGraphql, mutate: mockGraphql } }); }); afterEach(() => { diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/BindingAccountName.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/BindingAccountName.tsx index 179adcb..3aeae68 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/BindingAccountName.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/BindingAccountName.tsx @@ -13,7 +13,7 @@ * @see ConditionalStatus — conditional rendering with picklist data */ import { useEffect, useState } from 'react'; -import { createDataSDK, gql } from '@salesforce/sdk-data'; +import { createDataSDK, gql } from '@salesforce/platform-sdk'; // Every scalar field in UIAPI GraphQL is wrapped in { value }. // This is different from standard GraphQL where fields are plain scalars. @@ -55,7 +55,7 @@ export default function BindingAccountName() { useEffect(() => { const fetchName = async () => { const sdk = await createDataSDK(); - const result = await sdk.graphql?.({ query: QUERY }); + const result = await sdk.graphql?.query({ query: QUERY }); if (result?.errors?.length) { throw new Error( diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/ChildToParent.test.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/ChildToParent.test.tsx index 481e6b7..b602486 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/ChildToParent.test.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/ChildToParent.test.tsx @@ -8,11 +8,11 @@ import { render, screen } from '@testing-library/react'; import { type Mock } from 'vitest'; import userEvent from '@testing-library/user-event'; -import { createDataSDK } from '@salesforce/sdk-data'; +import { createDataSDK } from '@salesforce/platform-sdk'; import { axe } from 'vitest-axe'; import ChildToParent from './ChildToParent'; -vi.mock('@salesforce/sdk-data', () => ({ +vi.mock('@salesforce/platform-sdk', () => ({ createDataSDK: vi.fn(), gql: (strings: TemplateStringsArray) => strings.join(''), })); @@ -36,7 +36,7 @@ describe('ChildToParent', () => { const mockGraphql = vi.fn(); beforeEach(() => { - (createDataSDK as Mock).mockResolvedValue({ graphql: mockGraphql }); + (createDataSDK as Mock).mockResolvedValue({ graphql: { query: mockGraphql, mutate: mockGraphql } }); }); afterEach(() => { diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/ChildToParent.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/ChildToParent.tsx index a4d5606..69bf858 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/ChildToParent.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/ChildToParent.tsx @@ -12,7 +12,7 @@ * @see StateManagement — sharing state between sibling components */ import { useEffect, useState } from 'react'; -import { createDataSDK, gql } from '@salesforce/sdk-data'; +import { createDataSDK, gql } from '@salesforce/platform-sdk'; // Industry picklist values from the Account standard object const INDUSTRIES = [ @@ -74,7 +74,7 @@ export default function ChildToParent() { const fetchAccounts = async () => { const sdk = await createDataSDK(); - const result = await sdk.graphql?.({ + const result = await sdk.graphql?.query({ query: QUERY, variables: { industry }, }); diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/ConditionalStatus.test.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/ConditionalStatus.test.tsx index 40aace8..ada9acf 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/ConditionalStatus.test.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/ConditionalStatus.test.tsx @@ -6,11 +6,11 @@ */ import { render, screen } from '@testing-library/react'; import { type Mock } from 'vitest'; -import { createDataSDK } from '@salesforce/sdk-data'; +import { createDataSDK } from '@salesforce/platform-sdk'; import { axe } from 'vitest-axe'; import ConditionalStatus from './ConditionalStatus'; -vi.mock('@salesforce/sdk-data', () => ({ +vi.mock('@salesforce/platform-sdk', () => ({ createDataSDK: vi.fn(), gql: (strings: TemplateStringsArray) => strings.join(''), })); @@ -40,7 +40,7 @@ describe('ConditionalStatus', () => { const mockGraphql = vi.fn(); beforeEach(() => { - (createDataSDK as Mock).mockResolvedValue({ graphql: mockGraphql }); + (createDataSDK as Mock).mockResolvedValue({ graphql: { query: mockGraphql, mutate: mockGraphql } }); }); afterEach(() => { diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/ConditionalStatus.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/ConditionalStatus.tsx index c346efd..2b11877 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/ConditionalStatus.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/ConditionalStatus.tsx @@ -11,7 +11,7 @@ * @see ListOfAccounts — rendering a list of records with .map() */ import { useEffect, useState } from 'react'; -import { createDataSDK, gql } from '@salesforce/sdk-data'; +import { createDataSDK, gql } from '@salesforce/platform-sdk'; import { Badge } from '@/components/ui/badge'; const QUERY = gql` @@ -64,7 +64,7 @@ export default function ConditionalStatus() { useEffect(() => { const fetchAccount = async () => { const sdk = await createDataSDK(); - const result = await sdk.graphql?.({ query: QUERY }); + const result = await sdk.graphql?.query({ query: QUERY }); if (result?.errors?.length) { throw new Error( diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/LifecycleFetch.test.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/LifecycleFetch.test.tsx index 961bf2e..55154fa 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/LifecycleFetch.test.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/LifecycleFetch.test.tsx @@ -8,11 +8,11 @@ import { render, screen } from '@testing-library/react'; import { type Mock } from 'vitest'; import userEvent from '@testing-library/user-event'; -import { createDataSDK } from '@salesforce/sdk-data'; +import { createDataSDK } from '@salesforce/platform-sdk'; import { axe } from 'vitest-axe'; import LifecycleFetch from './LifecycleFetch'; -vi.mock('@salesforce/sdk-data', () => ({ +vi.mock('@salesforce/platform-sdk', () => ({ createDataSDK: vi.fn(), gql: (strings: TemplateStringsArray) => strings.join(''), })); @@ -42,7 +42,7 @@ describe('LifecycleFetch', () => { const mockGraphql = vi.fn(); beforeEach(() => { - (createDataSDK as Mock).mockResolvedValue({ graphql: mockGraphql }); + (createDataSDK as Mock).mockResolvedValue({ graphql: { query: mockGraphql, mutate: mockGraphql } }); }); afterEach(() => { diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/LifecycleFetch.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/LifecycleFetch.tsx index 3938426..d3a7917 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/LifecycleFetch.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/LifecycleFetch.tsx @@ -14,7 +14,7 @@ * @see SingleRecord — querying a single record with GraphQL */ import { useEffect, useState } from 'react'; -import { createDataSDK, gql } from '@salesforce/sdk-data'; +import { createDataSDK, gql } from '@salesforce/platform-sdk'; import { Button } from '@/components/ui/button'; const QUERY = gql` @@ -84,7 +84,7 @@ function ContactFetcher() { const fetchContact = async () => { const sdk = await createDataSDK(); - const result = await sdk.graphql?.({ query: QUERY }); + const result = await sdk.graphql?.query({ query: QUERY }); if (stale) return; // component was unmounted while fetching diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/ListOfAccounts.test.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/ListOfAccounts.test.tsx index 7d55dc0..591cb98 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/ListOfAccounts.test.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/ListOfAccounts.test.tsx @@ -1,5 +1,5 @@ /** - * The vi.mock() call below intercepts the @salesforce/sdk-data module for the + * The vi.mock() call below intercepts the @salesforce/platform-sdk module for the * entire test file. In LWC Jest the equivalent is registering a wire adapter * mock — here there is no wire adapter, so a plain module mock is enough. * The mock data objects mirror the real UIAPI GraphQL response shape so the @@ -7,11 +7,11 @@ */ import { render, screen } from '@testing-library/react'; import { type Mock } from 'vitest'; -import { createDataSDK } from '@salesforce/sdk-data'; +import { createDataSDK } from '@salesforce/platform-sdk'; import { axe } from 'vitest-axe'; import ListOfAccounts from './ListOfAccounts'; -vi.mock('@salesforce/sdk-data', () => ({ +vi.mock('@salesforce/platform-sdk', () => ({ createDataSDK: vi.fn(), gql: (strings: TemplateStringsArray) => strings.join(''), })); @@ -48,7 +48,7 @@ describe('ListOfAccounts', () => { const mockGraphql = vi.fn(); beforeEach(() => { - (createDataSDK as Mock).mockResolvedValue({ graphql: mockGraphql }); + (createDataSDK as Mock).mockResolvedValue({ graphql: { query: mockGraphql, mutate: mockGraphql } }); }); afterEach(() => { diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/ListOfAccounts.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/ListOfAccounts.tsx index 746dfc3..c6467e8 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/ListOfAccounts.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/ListOfAccounts.tsx @@ -11,7 +11,7 @@ * @see ParentToChild — passing data from parent to child components via props */ import { useEffect, useState } from 'react'; -import { createDataSDK, gql } from '@salesforce/sdk-data'; +import { createDataSDK, gql } from '@salesforce/platform-sdk'; const QUERY = gql` query AccountList { @@ -64,7 +64,7 @@ export default function ListOfAccounts() { useEffect(() => { const fetchAccounts = async () => { const sdk = await createDataSDK(); - const result = await sdk.graphql?.({ query: QUERY }); + const result = await sdk.graphql?.query({ query: QUERY }); if (result?.errors?.length) { throw new Error( diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/ParentToChild.test.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/ParentToChild.test.tsx index 7c2ba38..63e25a2 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/ParentToChild.test.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/ParentToChild.test.tsx @@ -10,11 +10,11 @@ */ import { render, screen } from '@testing-library/react'; import { type Mock } from 'vitest'; -import { createDataSDK } from '@salesforce/sdk-data'; +import { createDataSDK } from '@salesforce/platform-sdk'; import { axe } from 'vitest-axe'; import ParentToChild from './ParentToChild'; -vi.mock('@salesforce/sdk-data', () => ({ +vi.mock('@salesforce/platform-sdk', () => ({ createDataSDK: vi.fn(), gql: (strings: TemplateStringsArray) => strings.join(''), })); @@ -51,7 +51,7 @@ describe('ParentToChild', () => { const mockGraphql = vi.fn(); beforeEach(() => { - (createDataSDK as Mock).mockResolvedValue({ graphql: mockGraphql }); + (createDataSDK as Mock).mockResolvedValue({ graphql: { query: mockGraphql, mutate: mockGraphql } }); }); afterEach(() => { diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/ParentToChild.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/ParentToChild.tsx index a7b166f..7904930 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/ParentToChild.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/ParentToChild.tsx @@ -11,7 +11,7 @@ * @see ChildToParent — communicating from child back to parent via callbacks */ import { useEffect, useState } from 'react'; -import { createDataSDK, gql } from '@salesforce/sdk-data'; +import { createDataSDK, gql } from '@salesforce/platform-sdk'; const QUERY = gql` query TwoAccounts { @@ -64,7 +64,7 @@ export default function ParentToChild() { useEffect(() => { const fetchAccounts = async () => { const sdk = await createDataSDK(); - const result = await sdk.graphql?.({ query: QUERY }); + const result = await sdk.graphql?.query({ query: QUERY }); if (result?.errors?.length) { throw new Error( diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/StateManagement.test.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/StateManagement.test.tsx index 7d002ee..64fc378 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/StateManagement.test.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/StateManagement.test.tsx @@ -8,11 +8,11 @@ import { render, screen } from '@testing-library/react'; import { type Mock } from 'vitest'; import userEvent from '@testing-library/user-event'; -import { createDataSDK } from '@salesforce/sdk-data'; +import { createDataSDK } from '@salesforce/platform-sdk'; import { axe } from 'vitest-axe'; import StateManagement from './StateManagement'; -vi.mock('@salesforce/sdk-data', () => ({ +vi.mock('@salesforce/platform-sdk', () => ({ createDataSDK: vi.fn(), gql: (strings: TemplateStringsArray) => strings.join(''), })); @@ -49,7 +49,7 @@ describe('StateManagement', () => { const mockGraphql = vi.fn(); beforeEach(() => { - (createDataSDK as Mock).mockResolvedValue({ graphql: mockGraphql }); + (createDataSDK as Mock).mockResolvedValue({ graphql: { query: mockGraphql, mutate: mockGraphql } }); }); afterEach(() => { diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/StateManagement.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/StateManagement.tsx index 53d6948..c1d1721 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/StateManagement.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/hello/StateManagement.tsx @@ -13,7 +13,7 @@ * @see LifecycleFetch — cleanup patterns for async effects */ import { useEffect, useState } from 'react'; -import { createDataSDK, gql } from '@salesforce/sdk-data'; +import { createDataSDK, gql } from '@salesforce/platform-sdk'; import { Button } from '@/components/ui/button'; const QUERY = gql` @@ -68,7 +68,7 @@ export default function StateManagement() { useEffect(() => { const fetchAccounts = async () => { const sdk = await createDataSDK(); - const result = await sdk.graphql?.({ query: QUERY }); + const result = await sdk.graphql?.query({ query: QUERY }); if (result?.errors?.length) { throw new Error( diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/integration/DashboardAliasedQueries.test.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/integration/DashboardAliasedQueries.test.tsx index 8c808a8..022e9d7 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/integration/DashboardAliasedQueries.test.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/integration/DashboardAliasedQueries.test.tsx @@ -1,10 +1,10 @@ import { render, screen } from '@testing-library/react'; import { type Mock } from 'vitest'; -import { createDataSDK } from '@salesforce/sdk-data'; +import { createDataSDK } from '@salesforce/platform-sdk'; import { axe } from 'vitest-axe'; import DashboardAliasedQueries from './DashboardAliasedQueries'; -vi.mock('@salesforce/sdk-data', () => ({ +vi.mock('@salesforce/platform-sdk', () => ({ createDataSDK: vi.fn(), gql: (strings: TemplateStringsArray) => strings.join(''), })); @@ -32,7 +32,7 @@ describe('DashboardAliasedQueries', () => { const mockGraphql = vi.fn(); beforeEach(() => { - (createDataSDK as Mock).mockResolvedValue({ graphql: mockGraphql }); + (createDataSDK as Mock).mockResolvedValue({ graphql: { query: mockGraphql, mutate: mockGraphql } }); }); afterEach(() => { diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/integration/DashboardAliasedQueries.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/integration/DashboardAliasedQueries.tsx index 41a89f9..01a547b 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/integration/DashboardAliasedQueries.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/integration/DashboardAliasedQueries.tsx @@ -18,7 +18,7 @@ * a wrapper object with counts for each object type. */ import { useEffect, useState } from 'react'; -import { createDataSDK, gql } from '@salesforce/sdk-data'; +import { createDataSDK, gql } from '@salesforce/platform-sdk'; // Three aliases in one request — each becomes a key in the response const QUERY = gql` @@ -75,7 +75,7 @@ export default function DashboardAliasedQueries() { useEffect(() => { const fetchStats = async () => { const sdk = await createDataSDK(); - const result = await sdk.graphql?.({ query: QUERY }); + const result = await sdk.graphql?.query({ query: QUERY }); if (result?.errors?.length) { throw new Error( diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/integration/SearchableAccountList.test.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/integration/SearchableAccountList.test.tsx index a78f70c..df74c31 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/integration/SearchableAccountList.test.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/integration/SearchableAccountList.test.tsx @@ -1,10 +1,10 @@ import { render, screen, fireEvent, act } from '@testing-library/react'; import { type Mock } from 'vitest'; -import { createDataSDK } from '@salesforce/sdk-data'; +import { createDataSDK } from '@salesforce/platform-sdk'; import { axe } from 'vitest-axe'; import SearchableAccountList from './SearchableAccountList'; -vi.mock('@salesforce/sdk-data', () => ({ +vi.mock('@salesforce/platform-sdk', () => ({ createDataSDK: vi.fn(), gql: (strings: TemplateStringsArray) => strings.join(''), })); @@ -75,7 +75,7 @@ describe('SearchableAccountList', () => { const mockGraphql = vi.fn(); beforeEach(() => { - (createDataSDK as Mock).mockResolvedValue({ graphql: mockGraphql }); + (createDataSDK as Mock).mockResolvedValue({ graphql: { query: mockGraphql, mutate: mockGraphql } }); vi.useFakeTimers(); }); diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/integration/SearchableAccountList.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/integration/SearchableAccountList.tsx index 62ddf55..c339d68 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/integration/SearchableAccountList.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/integration/SearchableAccountList.tsx @@ -19,7 +19,7 @@ * @see DashboardAliasedQueries — combining aliased queries for dashboards */ import { useEffect, useState, useRef, useCallback } from 'react'; -import { createDataSDK, gql } from '@salesforce/sdk-data'; +import { createDataSDK, gql } from '@salesforce/platform-sdk'; import { Input } from '@/components/ui/input'; // The $name variable drives the `like` filter. Passing "%%" matches all. @@ -85,7 +85,7 @@ export default function SearchableAccountList() { try { const sdk = await createDataSDK(); // Wrap the search term in wildcards for the `like` operator - const result = await sdk.graphql?.({ + const result = await sdk.graphql?.query({ query: QUERY, variables: { name: term ? `%${term}%` : '%%' }, }); diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/modify-data/CreateRecord.test.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/modify-data/CreateRecord.test.tsx index e641638..d9ec9d5 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/modify-data/CreateRecord.test.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/modify-data/CreateRecord.test.tsx @@ -6,11 +6,11 @@ import { render, screen, fireEvent } from '@testing-library/react'; import { type Mock } from 'vitest'; import userEvent from '@testing-library/user-event'; -import { createDataSDK } from '@salesforce/sdk-data'; +import { createDataSDK } from '@salesforce/platform-sdk'; import { axe } from 'vitest-axe'; import CreateRecord from './CreateRecord'; -vi.mock('@salesforce/sdk-data', () => ({ +vi.mock('@salesforce/platform-sdk', () => ({ createDataSDK: vi.fn(), gql: (strings: TemplateStringsArray) => strings.join(''), })); @@ -30,7 +30,7 @@ describe('CreateRecord', () => { const mockGraphql = vi.fn(); beforeEach(() => { - (createDataSDK as Mock).mockResolvedValue({ graphql: mockGraphql }); + (createDataSDK as Mock).mockResolvedValue({ graphql: { query: mockGraphql, mutate: mockGraphql } }); }); afterEach(() => { @@ -80,7 +80,7 @@ describe('CreateRecord', () => { // here you assert the graphql function was called with a variables object. await screen.findByRole('status'); expect(mockGraphql).toHaveBeenCalledWith({ - query: expect.any(String), + mutation: expect.any(String), variables: expect.objectContaining({ input: { Account: { Name: 'New Account' } }, }), diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/modify-data/CreateRecord.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/modify-data/CreateRecord.tsx index 8a0a5fa..7cedcbd 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/modify-data/CreateRecord.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/modify-data/CreateRecord.tsx @@ -11,7 +11,7 @@ * @see UpdateRecord — editing an existing record */ import { useState, type FormEvent } from 'react'; -import { createDataSDK, gql } from '@salesforce/sdk-data'; +import { createDataSDK, gql } from '@salesforce/platform-sdk'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; @@ -57,8 +57,8 @@ export default function CreateRecord() { try { const sdk = await createDataSDK(); - const res = await sdk.graphql?.({ - query: CREATE_ACCOUNT, + const res = await sdk.graphql?.mutate({ + mutation: CREATE_ACCOUNT, variables: { input: { Account: { Name: name.trim() } } }, }); @@ -66,7 +66,7 @@ export default function CreateRecord() { throw new Error(res.errors.map((e: { message: string }) => e.message).join('; ')); } - const record = res?.data.uiapi?.AccountCreate?.Record; + const record = res?.data?.uiapi?.AccountCreate?.Record; if (!record) throw new Error('No record returned from AccountCreate'); setResult({ Id: record.Id, Name: record.Name?.value }); diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/modify-data/DeleteRecord.test.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/modify-data/DeleteRecord.test.tsx index b12ebe6..efde842 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/modify-data/DeleteRecord.test.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/modify-data/DeleteRecord.test.tsx @@ -6,11 +6,11 @@ import { render, screen, waitFor } from '@testing-library/react'; import { type Mock } from 'vitest'; import userEvent from '@testing-library/user-event'; -import { createDataSDK } from '@salesforce/sdk-data'; +import { createDataSDK } from '@salesforce/platform-sdk'; import { axe } from 'vitest-axe'; import DeleteRecord from './DeleteRecord'; -vi.mock('@salesforce/sdk-data', () => ({ +vi.mock('@salesforce/platform-sdk', () => ({ createDataSDK: vi.fn(), gql: (strings: TemplateStringsArray) => strings.join(''), })); @@ -52,7 +52,7 @@ describe('DeleteRecord', () => { const mockGraphql = vi.fn(); beforeEach(() => { - (createDataSDK as Mock).mockResolvedValue({ graphql: mockGraphql }); + (createDataSDK as Mock).mockResolvedValue({ graphql: { query: mockGraphql, mutate: mockGraphql } }); }); afterEach(() => { @@ -123,7 +123,7 @@ describe('DeleteRecord', () => { expect(screen.queryByText('Acme Corp')).not.toBeInTheDocument() ); expect(mockGraphql).toHaveBeenLastCalledWith({ - query: expect.any(String), + mutation: expect.any(String), variables: expect.objectContaining({ input: { Id: '001' } }), }); }); diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/modify-data/DeleteRecord.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/modify-data/DeleteRecord.tsx index c6548b5..1f4090f 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/modify-data/DeleteRecord.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/modify-data/DeleteRecord.tsx @@ -11,7 +11,7 @@ * @see QueryMutationTogether — inline editing with read-then-write cycle */ import { useEffect, useState } from 'react'; -import { createDataSDK, gql } from '@salesforce/sdk-data'; +import { createDataSDK, gql } from '@salesforce/platform-sdk'; import { Button } from '@/components/ui/button'; // ── Read: load Accounts to display ─────────────────────────────────────────── @@ -90,11 +90,11 @@ export default function DeleteRecord() { (async () => { try { const sdk = await createDataSDK(); - const res = await sdk.graphql?.({ query: LIST_QUERY }); + const res = await sdk.graphql?.query({ query: LIST_QUERY }); if (res?.errors?.length) { throw new Error(res.errors.map((e: { message: string }) => e.message).join('; ')); } - const nodes = (res?.data.uiapi?.query?.Account?.edges ?? []) + const nodes = (res?.data?.uiapi?.query?.Account?.edges ?? []) .map(edge => edge?.node) .filter((n): n is Account => n != null); setAccounts(nodes); @@ -112,8 +112,8 @@ export default function DeleteRecord() { try { const sdk = await createDataSDK(); - const res = await sdk.graphql?.({ - query: DELETE_MUTATION, + const res = await sdk.graphql?.mutate({ + mutation: DELETE_MUTATION, variables: { input: { Id: id } }, }); if (res?.errors?.length) { diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/modify-data/QueryMutationTogether.test.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/modify-data/QueryMutationTogether.test.tsx index 00be5b2..708422a 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/modify-data/QueryMutationTogether.test.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/modify-data/QueryMutationTogether.test.tsx @@ -5,11 +5,11 @@ import { render, screen, waitFor } from '@testing-library/react'; import { type Mock } from 'vitest'; import userEvent from '@testing-library/user-event'; -import { createDataSDK } from '@salesforce/sdk-data'; +import { createDataSDK } from '@salesforce/platform-sdk'; import { axe } from 'vitest-axe'; import QueryMutationTogether from './QueryMutationTogether'; -vi.mock('@salesforce/sdk-data', () => ({ +vi.mock('@salesforce/platform-sdk', () => ({ createDataSDK: vi.fn(), gql: (strings: TemplateStringsArray) => strings.join(''), })); @@ -61,7 +61,7 @@ describe('QueryMutationTogether', () => { const mockGraphql = vi.fn(); beforeEach(() => { - (createDataSDK as Mock).mockResolvedValue({ graphql: mockGraphql }); + (createDataSDK as Mock).mockResolvedValue({ graphql: { query: mockGraphql, mutate: mockGraphql } }); }); afterEach(() => { @@ -141,7 +141,7 @@ describe('QueryMutationTogether', () => { expect(screen.queryByRole('button', { name: 'Save' })).not.toBeInTheDocument() ); expect(mockGraphql).toHaveBeenLastCalledWith({ - query: expect.any(String), + mutation: expect.any(String), variables: expect.objectContaining({ input: expect.objectContaining({ Id: '001' }), }), diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/modify-data/QueryMutationTogether.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/modify-data/QueryMutationTogether.tsx index 122cf7b..66aeea5 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/modify-data/QueryMutationTogether.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/modify-data/QueryMutationTogether.tsx @@ -16,7 +16,7 @@ * @see ServerErrorHandling — handling mutation errors from the server */ import { useEffect, useState, type FormEvent } from 'react'; -import { createDataSDK, gql } from '@salesforce/sdk-data'; +import { createDataSDK, gql } from '@salesforce/platform-sdk'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; @@ -143,11 +143,11 @@ export default function QueryMutationTogether() { (async () => { try { const sdk = await createDataSDK(); - const res = await sdk.graphql?.({ query: LIST_QUERY }); + const res = await sdk.graphql?.query({ query: LIST_QUERY }); if (res?.errors?.length) { throw new Error(res.errors.map((e: { message: string }) => e.message).join('; ')); } - const nodes = (res?.data.uiapi?.query?.Account?.edges ?? []) + const nodes = (res?.data?.uiapi?.query?.Account?.edges ?? []) .map(edge => edge?.node) .filter((n): n is Account => n != null); setAccounts(nodes); @@ -180,8 +180,8 @@ export default function QueryMutationTogether() { try { const sdk = await createDataSDK(); - const res = await sdk.graphql?.({ - query: UPDATE_MUTATION, + const res = await sdk.graphql?.mutate({ + mutation: UPDATE_MUTATION, variables: { input: { Id: editId, @@ -194,7 +194,7 @@ export default function QueryMutationTogether() { throw new Error(res.errors.map((e: { message: string }) => e.message).join('; ')); } - const record = res?.data.uiapi?.AccountUpdate?.Record; + const record = res?.data?.uiapi?.AccountUpdate?.Record; if (!record) throw new Error('No record returned from AccountUpdate'); // Update the single row in local state using the server response — diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/modify-data/ServerErrorHandling.test.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/modify-data/ServerErrorHandling.test.tsx index 80d97f0..3dbca4e 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/modify-data/ServerErrorHandling.test.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/modify-data/ServerErrorHandling.test.tsx @@ -6,11 +6,11 @@ import { render, screen } from '@testing-library/react'; import { type Mock } from 'vitest'; import userEvent from '@testing-library/user-event'; -import { createDataSDK } from '@salesforce/sdk-data'; +import { createDataSDK } from '@salesforce/platform-sdk'; import { axe } from 'vitest-axe'; import ServerErrorHandling from './ServerErrorHandling'; -vi.mock('@salesforce/sdk-data', () => ({ +vi.mock('@salesforce/platform-sdk', () => ({ createDataSDK: vi.fn(), gql: (strings: TemplateStringsArray) => strings.join(''), })); @@ -19,7 +19,7 @@ describe('ServerErrorHandling', () => { const mockGraphql = vi.fn(); beforeEach(() => { - (createDataSDK as Mock).mockResolvedValue({ graphql: mockGraphql }); + (createDataSDK as Mock).mockResolvedValue({ graphql: { query: mockGraphql, mutate: mockGraphql } }); }); afterEach(() => { diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/modify-data/ServerErrorHandling.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/modify-data/ServerErrorHandling.tsx index dfeef6b..9f196f2 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/modify-data/ServerErrorHandling.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/modify-data/ServerErrorHandling.tsx @@ -17,7 +17,7 @@ * @see LoadingErrorEmpty — handling loading, error, and empty UI states */ import { useState, type FormEvent } from 'react'; -import { createDataSDK, gql } from '@salesforce/sdk-data'; +import { createDataSDK, gql } from '@salesforce/platform-sdk'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; @@ -60,8 +60,8 @@ export default function ServerErrorHandling() { try { const sdk = await createDataSDK(); - const result = await sdk.graphql?.({ - query: CREATE_CONTACT, + const result = await sdk.graphql?.mutate({ + mutation: CREATE_CONTACT, variables: { input: { Contact: { @@ -80,7 +80,7 @@ export default function ServerErrorHandling() { return; } - const payload = result?.data.uiapi?.ContactCreate; + const payload = result?.data?.uiapi?.ContactCreate; // Success: no top-level errors setCreatedId(payload?.Record?.Id); diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/modify-data/UpdateRecord.test.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/modify-data/UpdateRecord.test.tsx index 0e565c4..4e13a31 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/modify-data/UpdateRecord.test.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/modify-data/UpdateRecord.test.tsx @@ -6,11 +6,11 @@ import { render, screen } from '@testing-library/react'; import { type Mock } from 'vitest'; import userEvent from '@testing-library/user-event'; -import { createDataSDK } from '@salesforce/sdk-data'; +import { createDataSDK } from '@salesforce/platform-sdk'; import { axe } from 'vitest-axe'; import UpdateRecord from './UpdateRecord'; -vi.mock('@salesforce/sdk-data', () => ({ +vi.mock('@salesforce/platform-sdk', () => ({ createDataSDK: vi.fn(), gql: (strings: TemplateStringsArray) => strings.join(''), })); @@ -55,7 +55,7 @@ describe('UpdateRecord', () => { const mockGraphql = vi.fn(); beforeEach(() => { - (createDataSDK as Mock).mockResolvedValue({ graphql: mockGraphql }); + (createDataSDK as Mock).mockResolvedValue({ graphql: { query: mockGraphql, mutate: mockGraphql } }); }); afterEach(() => { @@ -128,7 +128,7 @@ describe('UpdateRecord', () => { // Id is a top-level field on the input, not nested inside Account — // verify the correct shape is sent to the mutation. expect(mockGraphql).toHaveBeenLastCalledWith({ - query: expect.any(String), + mutation: expect.any(String), variables: expect.objectContaining({ input: expect.objectContaining({ Id: '001' }), }), diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/modify-data/UpdateRecord.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/modify-data/UpdateRecord.tsx index df8784d..778908e 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/modify-data/UpdateRecord.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/modify-data/UpdateRecord.tsx @@ -11,7 +11,7 @@ * @see DeleteRecord — removing records with a confirmation pattern */ import { useEffect, useState, type FormEvent } from 'react'; -import { createDataSDK, gql } from '@salesforce/sdk-data'; +import { createDataSDK, gql } from '@salesforce/platform-sdk'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; @@ -130,11 +130,11 @@ export default function UpdateRecord() { (async () => { try { const sdk = await createDataSDK(); - const res = await sdk.graphql?.({ query: LOAD_QUERY }); + const res = await sdk.graphql?.query({ query: LOAD_QUERY }); if (res?.errors?.length) { throw new Error(res.errors.map((e: { message: string }) => e.message).join('; ')); } - const node = res?.data.uiapi?.query?.Account?.edges?.[0]?.node; + const node = res?.data?.uiapi?.query?.Account?.edges?.[0]?.node; if (node) { setAccountId(node.Id); setName(node.Name?.value ?? ''); @@ -158,8 +158,8 @@ export default function UpdateRecord() { try { const sdk = await createDataSDK(); - const res = await sdk.graphql?.({ - query: UPDATE_MUTATION, + const res = await sdk.graphql?.mutate({ + mutation: UPDATE_MUTATION, variables: { // Id is a top-level field on the input, not nested inside Account input: { Id: accountId, Account: { Name: name, Industry: industry } }, @@ -170,7 +170,7 @@ export default function UpdateRecord() { throw new Error(res.errors.map((e: { message: string }) => e.message).join('; ')); } - const record = res?.data.uiapi?.AccountUpdate?.Record; + const record = res?.data?.uiapi?.AccountUpdate?.Record; if (!record) throw new Error('No record returned from AccountUpdate'); // Sync local state with values returned from the server response diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/AliasedMultiObjectQuery.test.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/AliasedMultiObjectQuery.test.tsx index ff5fc2d..7bab8f7 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/AliasedMultiObjectQuery.test.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/AliasedMultiObjectQuery.test.tsx @@ -6,11 +6,11 @@ */ import { render, screen } from '@testing-library/react'; import { type Mock } from 'vitest'; -import { createDataSDK } from '@salesforce/sdk-data'; +import { createDataSDK } from '@salesforce/platform-sdk'; import { axe } from 'vitest-axe'; import AliasedMultiObjectQuery from './AliasedMultiObjectQuery'; -vi.mock('@salesforce/sdk-data', () => ({ +vi.mock('@salesforce/platform-sdk', () => ({ createDataSDK: vi.fn(), gql: (strings: TemplateStringsArray) => strings.join(''), })); @@ -35,7 +35,7 @@ describe('AliasedMultiObjectQuery', () => { const mockGraphql = vi.fn(); beforeEach(() => { - (createDataSDK as Mock).mockResolvedValue({ graphql: mockGraphql }); + (createDataSDK as Mock).mockResolvedValue({ graphql: { query: mockGraphql, mutate: mockGraphql } }); }); afterEach(() => { diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/AliasedMultiObjectQuery.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/AliasedMultiObjectQuery.tsx index 46acf0f..475a8f9 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/AliasedMultiObjectQuery.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/AliasedMultiObjectQuery.tsx @@ -17,7 +17,7 @@ * @see ImperativeRefetch — re-fetching data on demand */ import { useEffect, useState } from 'react'; -import { createDataSDK, gql } from '@salesforce/sdk-data'; +import { createDataSDK, gql } from '@salesforce/platform-sdk'; // Aliases (accounts: Account, contacts: Contact) let us query two objects // in one round-trip. Each alias becomes a key in the response data. @@ -68,7 +68,7 @@ export default function AliasedMultiObjectQuery() { useEffect(() => { const fetchCounts = async () => { const sdk = await createDataSDK(); - const result = await sdk.graphql?.({ query: QUERY }); + const result = await sdk.graphql?.query({ query: QUERY }); if (result?.errors?.length) { throw new Error( diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/FilteredList.test.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/FilteredList.test.tsx index 5d0a694..c696e79 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/FilteredList.test.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/FilteredList.test.tsx @@ -6,11 +6,11 @@ */ import { render, screen, fireEvent, act } from '@testing-library/react'; import { type Mock } from 'vitest'; -import { createDataSDK } from '@salesforce/sdk-data'; +import { createDataSDK } from '@salesforce/platform-sdk'; import { axe } from 'vitest-axe'; import FilteredList from './FilteredList'; -vi.mock('@salesforce/sdk-data', () => ({ +vi.mock('@salesforce/platform-sdk', () => ({ createDataSDK: vi.fn(), gql: (strings: TemplateStringsArray) => strings.join(''), })); @@ -42,7 +42,7 @@ describe('FilteredList', () => { const mockGraphql = vi.fn(); beforeEach(() => { - (createDataSDK as Mock).mockResolvedValue({ graphql: mockGraphql }); + (createDataSDK as Mock).mockResolvedValue({ graphql: { query: mockGraphql, mutate: mockGraphql } }); vi.useFakeTimers(); }); diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/FilteredList.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/FilteredList.tsx index 0f0d719..e57218d 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/FilteredList.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/FilteredList.tsx @@ -12,7 +12,7 @@ */ import { useEffect, useState } from 'react'; -import { createDataSDK, gql } from '@salesforce/sdk-data'; +import { createDataSDK, gql } from '@salesforce/platform-sdk'; import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'; import { Input } from '@/components/ui/input'; @@ -94,7 +94,7 @@ export default function FilteredList() { const fetchFiltered = async () => { const sdk = await createDataSDK(); // Wrap the search term in % wildcards for the `like` operator - const result = await sdk.graphql?.({ + const result = await sdk.graphql?.query({ query: QUERY, variables: { name: `%${search}%` }, }); diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/ImperativeRefetch.test.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/ImperativeRefetch.test.tsx index 48fd819..0a33a78 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/ImperativeRefetch.test.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/ImperativeRefetch.test.tsx @@ -7,11 +7,11 @@ import { render, screen, fireEvent } from '@testing-library/react'; import { type Mock } from 'vitest'; import userEvent from '@testing-library/user-event'; -import { createDataSDK } from '@salesforce/sdk-data'; +import { createDataSDK } from '@salesforce/platform-sdk'; import { axe } from 'vitest-axe'; import ImperativeRefetch from './ImperativeRefetch'; -vi.mock('@salesforce/sdk-data', () => ({ +vi.mock('@salesforce/platform-sdk', () => ({ createDataSDK: vi.fn(), gql: (strings: TemplateStringsArray) => strings.join(''), })); @@ -41,7 +41,7 @@ describe('ImperativeRefetch', () => { const mockGraphql = vi.fn(); beforeEach(() => { - (createDataSDK as Mock).mockResolvedValue({ graphql: mockGraphql }); + (createDataSDK as Mock).mockResolvedValue({ graphql: { query: mockGraphql, mutate: mockGraphql } }); }); afterEach(() => { diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/ImperativeRefetch.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/ImperativeRefetch.tsx index 043f220..3f7b436 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/ImperativeRefetch.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/ImperativeRefetch.tsx @@ -13,7 +13,7 @@ * @see CreateRecord — creating new records with GraphQL mutations */ import { useEffect, useState, useCallback } from 'react'; -import { createDataSDK, gql } from '@salesforce/sdk-data'; +import { createDataSDK, gql } from '@salesforce/platform-sdk'; import { Button } from '@/components/ui/button'; const QUERY = gql` @@ -73,7 +73,7 @@ export default function ImperativeRefetch() { try { const sdk = await createDataSDK(); - const result = await sdk.graphql?.({ query: QUERY }); + const result = await sdk.graphql?.query({ query: QUERY }); if (result?.errors?.length) { throw new Error( diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/ListOfRecords.test.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/ListOfRecords.test.tsx index c4aa424..a9dcb33 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/ListOfRecords.test.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/ListOfRecords.test.tsx @@ -6,11 +6,11 @@ */ import { render, screen } from '@testing-library/react'; import { type Mock } from 'vitest'; -import { createDataSDK } from '@salesforce/sdk-data'; +import { createDataSDK } from '@salesforce/platform-sdk'; import { axe } from 'vitest-axe'; import ListOfRecords from './ListOfRecords'; -vi.mock('@salesforce/sdk-data', () => ({ +vi.mock('@salesforce/platform-sdk', () => ({ createDataSDK: vi.fn(), gql: (strings: TemplateStringsArray) => strings.join(''), })); @@ -51,7 +51,7 @@ describe('ListOfRecords', () => { const mockGraphql = vi.fn(); beforeEach(() => { - (createDataSDK as Mock).mockResolvedValue({ graphql: mockGraphql }); + (createDataSDK as Mock).mockResolvedValue({ graphql: { query: mockGraphql, mutate: mockGraphql } }); }); afterEach(() => { diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/ListOfRecords.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/ListOfRecords.tsx index 93443c5..5582f0c 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/ListOfRecords.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/ListOfRecords.tsx @@ -12,7 +12,7 @@ * @see FilteredList — adding a search filter with GraphQL variables */ import { useEffect, useState } from 'react'; -import { createDataSDK, gql } from '@salesforce/sdk-data'; +import { createDataSDK, gql } from '@salesforce/platform-sdk'; // Relay connection pattern: the response wraps records in edges[].node. const QUERY = gql` @@ -82,7 +82,7 @@ export default function ListOfRecords() { useEffect(() => { const fetchContacts = async () => { const sdk = await createDataSDK(); - const result = await sdk.graphql?.({ query: QUERY }); + const result = await sdk.graphql?.query({ query: QUERY }); if (result?.errors?.length) { throw new Error( diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/PaginatedList.test.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/PaginatedList.test.tsx index 816c89a..a3fec05 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/PaginatedList.test.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/PaginatedList.test.tsx @@ -7,11 +7,11 @@ import { render, screen } from '@testing-library/react'; import { type Mock } from 'vitest'; import userEvent from '@testing-library/user-event'; -import { createDataSDK } from '@salesforce/sdk-data'; +import { createDataSDK } from '@salesforce/platform-sdk'; import { axe } from 'vitest-axe'; import PaginatedList from './PaginatedList'; -vi.mock('@salesforce/sdk-data', () => ({ +vi.mock('@salesforce/platform-sdk', () => ({ createDataSDK: vi.fn(), gql: (strings: TemplateStringsArray) => strings.join(''), })); @@ -100,7 +100,7 @@ describe('PaginatedList', () => { const mockGraphql = vi.fn(); beforeEach(() => { - (createDataSDK as Mock).mockResolvedValue({ graphql: mockGraphql }); + (createDataSDK as Mock).mockResolvedValue({ graphql: { query: mockGraphql, mutate: mockGraphql } }); }); afterEach(() => { diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/PaginatedList.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/PaginatedList.tsx index 3629712..ba04791 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/PaginatedList.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/PaginatedList.tsx @@ -17,7 +17,7 @@ * @see RelatedRecords — traversing parent-child relationships */ import { useEffect, useState } from 'react'; -import { createDataSDK, gql } from '@salesforce/sdk-data'; +import { createDataSDK, gql } from '@salesforce/platform-sdk'; import { Button } from '@/components/ui/button'; // The $after variable is a cursor string returned by pageInfo.endCursor. @@ -87,7 +87,7 @@ export default function PaginatedList() { const sdk = await createDataSDK(); // Pass the cursor as a GraphQL variable — omit for the first page const variables = after ? { after } : {}; - const result = await sdk.graphql?.({ + const result = await sdk.graphql?.query({ query: QUERY, variables, }); diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/RelatedRecords.test.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/RelatedRecords.test.tsx index 33184ec..c1174a1 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/RelatedRecords.test.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/RelatedRecords.test.tsx @@ -6,11 +6,11 @@ */ import { render, screen } from '@testing-library/react'; import { type Mock } from 'vitest'; -import { createDataSDK } from '@salesforce/sdk-data'; +import { createDataSDK } from '@salesforce/platform-sdk'; import { axe } from 'vitest-axe'; import RelatedRecords from './RelatedRecords'; -vi.mock('@salesforce/sdk-data', () => ({ +vi.mock('@salesforce/platform-sdk', () => ({ createDataSDK: vi.fn(), gql: (strings: TemplateStringsArray) => strings.join(''), })); @@ -49,7 +49,7 @@ describe('RelatedRecords', () => { const mockGraphql = vi.fn(); beforeEach(() => { - (createDataSDK as Mock).mockResolvedValue({ graphql: mockGraphql }); + (createDataSDK as Mock).mockResolvedValue({ graphql: { query: mockGraphql, mutate: mockGraphql } }); }); afterEach(() => { diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/RelatedRecords.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/RelatedRecords.tsx index 491f76e..353658f 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/RelatedRecords.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/RelatedRecords.tsx @@ -16,7 +16,7 @@ * @see AliasedMultiObjectQuery — querying multiple objects in one request */ import { useEffect, useState } from 'react'; -import { createDataSDK, gql } from '@salesforce/sdk-data'; +import { createDataSDK, gql } from '@salesforce/platform-sdk'; // To traverse a lookup, nest the parent object's fields under the // relationship name. Here Contact.AccountId becomes `Account { Name { value } }`. @@ -81,7 +81,7 @@ export default function RelatedRecords() { useEffect(() => { const fetchContacts = async () => { const sdk = await createDataSDK(); - const result = await sdk.graphql?.({ query: QUERY }); + const result = await sdk.graphql?.query({ query: QUERY }); if (result?.errors?.length) { throw new Error( diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/SingleRecord.test.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/SingleRecord.test.tsx index 2c57470..a18adb5 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/SingleRecord.test.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/SingleRecord.test.tsx @@ -6,11 +6,11 @@ */ import { render, screen } from '@testing-library/react'; import { type Mock } from 'vitest'; -import { createDataSDK } from '@salesforce/sdk-data'; +import { createDataSDK } from '@salesforce/platform-sdk'; import { axe } from 'vitest-axe'; import SingleRecord from './SingleRecord'; -vi.mock('@salesforce/sdk-data', () => ({ +vi.mock('@salesforce/platform-sdk', () => ({ createDataSDK: vi.fn(), gql: (strings: TemplateStringsArray) => strings.join(''), })); @@ -42,7 +42,7 @@ describe('SingleRecord', () => { const mockGraphql = vi.fn(); beforeEach(() => { - (createDataSDK as Mock).mockResolvedValue({ graphql: mockGraphql }); + (createDataSDK as Mock).mockResolvedValue({ graphql: { query: mockGraphql, mutate: mockGraphql } }); }); afterEach(() => { diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/SingleRecord.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/SingleRecord.tsx index a411f94..42c48f2 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/SingleRecord.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/SingleRecord.tsx @@ -12,7 +12,7 @@ * @see ListOfRecords — querying multiple records */ import { useEffect, useState } from 'react'; -import { createDataSDK, gql } from '@salesforce/sdk-data'; +import { createDataSDK, gql } from '@salesforce/platform-sdk'; import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'; // UIAPI GraphQL wraps every query under uiapi.query.. @@ -88,7 +88,7 @@ export default function SingleRecord() { useEffect(() => { const fetchContact = async () => { const sdk = await createDataSDK(); - const result = await sdk.graphql?.({ query: QUERY }); + const result = await sdk.graphql?.query({ query: QUERY }); // GraphQL errors don't throw — they're returned in the errors array // alongside partial data. Always check before reading data. diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/SortedResults.test.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/SortedResults.test.tsx index aeb9917..31c527f 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/SortedResults.test.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/SortedResults.test.tsx @@ -7,11 +7,11 @@ import { render, screen } from '@testing-library/react'; import { type Mock } from 'vitest'; import userEvent from '@testing-library/user-event'; -import { createDataSDK } from '@salesforce/sdk-data'; +import { createDataSDK } from '@salesforce/platform-sdk'; import { axe } from 'vitest-axe'; import SortedResults from './SortedResults'; -vi.mock('@salesforce/sdk-data', () => ({ +vi.mock('@salesforce/platform-sdk', () => ({ createDataSDK: vi.fn(), gql: (strings: TemplateStringsArray) => strings.join(''), })); @@ -48,7 +48,7 @@ describe('SortedResults', () => { const mockGraphql = vi.fn(); beforeEach(() => { - (createDataSDK as Mock).mockResolvedValue({ graphql: mockGraphql }); + (createDataSDK as Mock).mockResolvedValue({ graphql: { query: mockGraphql, mutate: mockGraphql } }); }); afterEach(() => { diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/SortedResults.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/SortedResults.tsx index 374a07f..c2622a2 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/SortedResults.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/read-data/SortedResults.tsx @@ -13,7 +13,7 @@ */ import { useEffect, useState } from 'react'; -import { createDataSDK } from '@salesforce/sdk-data'; +import { createDataSDK } from '@salesforce/platform-sdk'; type SortField = 'Name' | 'Title' | 'Phone'; type SortDir = 'ASC' | 'DESC'; @@ -87,7 +87,7 @@ export default function SortedResults() { const fetchSorted = async () => { const sdk = await createDataSDK(); - const result = await sdk.graphql?.({ + const result = await sdk.graphql?.query({ query: buildQuery(field, dir), }); diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/routing/NestedRoutes.test.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/routing/NestedRoutes.test.tsx index ab9a4e7..963e69e 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/routing/NestedRoutes.test.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/routing/NestedRoutes.test.tsx @@ -9,14 +9,14 @@ import { render, screen } from '@testing-library/react'; import { type Mock } from 'vitest'; import { MemoryRouter, Outlet, Route, Routes } from 'react-router'; -import { createDataSDK } from '@salesforce/sdk-data'; +import { createDataSDK } from '@salesforce/platform-sdk'; import { axe } from 'vitest-axe'; import NestedRoutes, { NestedRoutesIndex, NestedRoutesDetail, } from './NestedRoutes'; -vi.mock('@salesforce/sdk-data', () => ({ +vi.mock('@salesforce/platform-sdk', () => ({ createDataSDK: vi.fn(), gql: (strings: TemplateStringsArray) => strings.join(''), })); @@ -49,7 +49,7 @@ describe('NestedRoutes (layout)', () => { const mockGraphql = vi.fn(); beforeEach(() => { - (createDataSDK as Mock).mockResolvedValue({ graphql: mockGraphql }); + (createDataSDK as Mock).mockResolvedValue({ graphql: { query: mockGraphql, mutate: mockGraphql } }); }); afterEach(() => { diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/routing/NestedRoutes.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/routing/NestedRoutes.tsx index 6af649d..bff4b48 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/routing/NestedRoutes.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/routing/NestedRoutes.tsx @@ -18,7 +18,7 @@ */ import { useEffect, useState } from 'react'; import { Link, Outlet, useOutletContext, useParams } from 'react-router'; -import { createDataSDK, gql } from '@salesforce/sdk-data'; +import { createDataSDK, gql } from '@salesforce/platform-sdk'; const QUERY = gql` query AccountsForNesting { @@ -71,7 +71,7 @@ export default function NestedRoutes() { useEffect(() => { const fetchAccounts = async () => { const sdk = await createDataSDK(); - const result = await sdk.graphql?.({ query: QUERY }); + const result = await sdk.graphql?.query({ query: QUERY }); if (result?.errors?.length) { throw new Error( diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/routing/RouteParameters.test.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/routing/RouteParameters.test.tsx index 946fd10..11a2dcc 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/routing/RouteParameters.test.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/routing/RouteParameters.test.tsx @@ -5,14 +5,14 @@ import { render, screen } from '@testing-library/react'; import { type Mock } from 'vitest'; import { MemoryRouter, Route, Routes } from 'react-router'; -import { createDataSDK } from '@salesforce/sdk-data'; +import { createDataSDK } from '@salesforce/platform-sdk'; import { axe } from 'vitest-axe'; import { RouteParametersList, RouteParametersDetail, } from './RouteParameters'; -vi.mock('@salesforce/sdk-data', () => ({ +vi.mock('@salesforce/platform-sdk', () => ({ createDataSDK: vi.fn(), gql: (strings: TemplateStringsArray) => strings.join(''), })); @@ -72,7 +72,7 @@ describe('RouteParametersList', () => { const mockGraphql = vi.fn(); beforeEach(() => { - (createDataSDK as Mock).mockResolvedValue({ graphql: mockGraphql }); + (createDataSDK as Mock).mockResolvedValue({ graphql: { query: mockGraphql, mutate: mockGraphql } }); }); afterEach(() => { @@ -122,7 +122,7 @@ describe('RouteParametersDetail', () => { const mockGraphql = vi.fn(); beforeEach(() => { - (createDataSDK as Mock).mockResolvedValue({ graphql: mockGraphql }); + (createDataSDK as Mock).mockResolvedValue({ graphql: { query: mockGraphql, mutate: mockGraphql } }); }); afterEach(() => { diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/routing/RouteParameters.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/routing/RouteParameters.tsx index 676484d..d7965c4 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/routing/RouteParameters.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/routing/RouteParameters.tsx @@ -16,7 +16,7 @@ */ import { useEffect, useState } from 'react'; import { Link, useParams } from 'react-router'; -import { createDataSDK, gql } from '@salesforce/sdk-data'; +import { createDataSDK, gql } from '@salesforce/platform-sdk'; const LIST_QUERY = gql` query AccountsForRouting { @@ -113,7 +113,7 @@ export function RouteParametersList() { useEffect(() => { const fetch = async () => { const sdk = await createDataSDK(); - const result = await sdk.graphql?.({ query: LIST_QUERY }); + const result = await sdk.graphql?.query({ query: LIST_QUERY }); if (result?.errors?.length) { throw new Error( @@ -177,7 +177,7 @@ export function RouteParametersDetail() { const fetch = async () => { const sdk = await createDataSDK(); - const result = await sdk.graphql?.({ + const result = await sdk.graphql?.query({ query: DETAIL_QUERY, variables: { id: accountId }, }); diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/salesforce-apis/ApexRest.test.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/salesforce-apis/ApexRest.test.tsx index 65441b1..0d3af76 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/salesforce-apis/ApexRest.test.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/salesforce-apis/ApexRest.test.tsx @@ -9,11 +9,11 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { type Mock } from 'vitest'; -import { createDataSDK } from '@salesforce/sdk-data'; +import { createDataSDK } from '@salesforce/platform-sdk'; import { axe } from 'vitest-axe'; import ApexRest from './ApexRest'; -vi.mock('@salesforce/sdk-data', () => ({ +vi.mock('@salesforce/platform-sdk', () => ({ createDataSDK: vi.fn(), })); diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/salesforce-apis/ApexRest.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/salesforce-apis/ApexRest.tsx index c809161..bf7eb54 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/salesforce-apis/ApexRest.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/salesforce-apis/ApexRest.tsx @@ -11,7 +11,7 @@ * @see UiApiRest — using the UI API REST endpoints directly */ import { useState, type FormEvent } from 'react'; -import { createDataSDK } from '@salesforce/sdk-data'; +import { createDataSDK } from '@salesforce/platform-sdk'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Avatar, AvatarImage } from '@/components/ui/avatar'; diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/salesforce-apis/ConnectApi.test.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/salesforce-apis/ConnectApi.test.tsx index 2304475..bccb3bc 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/salesforce-apis/ConnectApi.test.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/salesforce-apis/ConnectApi.test.tsx @@ -7,11 +7,11 @@ */ import { render, screen } from '@testing-library/react'; import { type Mock } from 'vitest'; -import { createDataSDK } from '@salesforce/sdk-data'; +import { createDataSDK } from '@salesforce/platform-sdk'; import { axe } from 'vitest-axe'; import ConnectApi from './ConnectApi'; -vi.mock('@salesforce/sdk-data', () => ({ +vi.mock('@salesforce/platform-sdk', () => ({ createDataSDK: vi.fn(), })); diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/salesforce-apis/ConnectApi.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/salesforce-apis/ConnectApi.tsx index c1a5159..40110b7 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/salesforce-apis/ConnectApi.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/salesforce-apis/ConnectApi.tsx @@ -11,7 +11,7 @@ * @see ApexRest — calling custom Apex REST endpoints */ import { useEffect, useState } from 'react'; -import { createDataSDK } from '@salesforce/sdk-data'; +import { createDataSDK } from '@salesforce/platform-sdk'; import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'; // Minimal shape of a Chatter feed-elements response diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/salesforce-apis/DisplayCurrentUser.test.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/salesforce-apis/DisplayCurrentUser.test.tsx index 710131a..4a27df8 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/salesforce-apis/DisplayCurrentUser.test.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/salesforce-apis/DisplayCurrentUser.test.tsx @@ -3,11 +3,11 @@ */ import { render, screen } from '@testing-library/react'; import { type Mock } from 'vitest'; -import { createDataSDK } from '@salesforce/sdk-data'; +import { createDataSDK } from '@salesforce/platform-sdk'; import { axe } from 'vitest-axe'; import DisplayCurrentUser from './DisplayCurrentUser'; -vi.mock('@salesforce/sdk-data', () => ({ +vi.mock('@salesforce/platform-sdk', () => ({ createDataSDK: vi.fn(), })); diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/salesforce-apis/DisplayCurrentUser.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/salesforce-apis/DisplayCurrentUser.tsx index 60f6b91..98c1efb 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/salesforce-apis/DisplayCurrentUser.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/salesforce-apis/DisplayCurrentUser.tsx @@ -11,7 +11,7 @@ * @see ConnectApi — fetching Chatter feed data via Connect API */ import { useEffect, useState } from 'react'; -import { createDataSDK } from '@salesforce/sdk-data'; +import { createDataSDK } from '@salesforce/platform-sdk'; interface ChatterUser { displayName: string; diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/salesforce-apis/UiApiRest.test.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/salesforce-apis/UiApiRest.test.tsx index 681ee4b..bcfcf43 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/salesforce-apis/UiApiRest.test.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/salesforce-apis/UiApiRest.test.tsx @@ -4,11 +4,11 @@ */ import { render, screen } from '@testing-library/react'; import { type Mock } from 'vitest'; -import { createDataSDK } from '@salesforce/sdk-data'; +import { createDataSDK } from '@salesforce/platform-sdk'; import { axe } from 'vitest-axe'; import UiApiRest from './UiApiRest'; -vi.mock('@salesforce/sdk-data', () => ({ +vi.mock('@salesforce/platform-sdk', () => ({ createDataSDK: vi.fn(), })); diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/salesforce-apis/UiApiRest.tsx b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/salesforce-apis/UiApiRest.tsx index 5839bc3..7478b0c 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/salesforce-apis/UiApiRest.tsx +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/src/recipes/salesforce-apis/UiApiRest.tsx @@ -13,7 +13,7 @@ * @see ButtonSLDS — styling with SLDS blueprint CSS classes */ import { useEffect, useState } from 'react'; -import { createDataSDK } from '@salesforce/sdk-data'; +import { createDataSDK } from '@salesforce/platform-sdk'; import { Avatar, AvatarImage } from '@/components/ui/avatar'; // UI API list-ui response shape diff --git a/force-app/main/react-recipes/uiBundles/reactRecipes/tsconfig.json b/force-app/main/react-recipes/uiBundles/reactRecipes/tsconfig.json index 3fc46eb..1e148d5 100644 --- a/force-app/main/react-recipes/uiBundles/reactRecipes/tsconfig.json +++ b/force-app/main/react-recipes/uiBundles/reactRecipes/tsconfig.json @@ -12,7 +12,6 @@ "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, - "allowJs": true, "jsx": "react-jsx", /* Linting */ From eebf5675160f4b938a7100eb6af92a64151eb578 Mon Sep 17 00:00:00 2001 From: AMAN SINGH Date: Tue, 14 Jul 2026 23:34:17 +0530 Subject: [PATCH 05/10] updated ga recipes --- .../lwc/mfeDirtyState/mfeDirtyState.html | 13 ++++- .../lwc/mfeDirtyState/mfeDirtyState.js | 15 +++++- .../lwc/mfeSendEvent/mfeSendEvent.html | 22 +++++++-- .../default/lwc/mfeSendEvent/mfeSendEvent.js | 36 +++++++++++++- mfe-app/package-lock.json | 38 +++++++-------- mfe-app/package.json | 2 +- mfe-app/src/recipes/AutoResize.tsx | 20 ++++---- mfe-app/src/recipes/BasicEmbed.tsx | 20 ++++---- mfe-app/src/recipes/DirtyState.tsx | 18 ++++++- mfe-app/src/recipes/ReceiveData.tsx | 44 ++++++++--------- mfe-app/src/recipes/SendEvent.tsx | 26 ++++++---- mfe-app/src/recipes/ThemeTokens.tsx | 47 +++++++++++-------- 12 files changed, 197 insertions(+), 104 deletions(-) diff --git a/force-app/main/default/lwc/mfeDirtyState/mfeDirtyState.html b/force-app/main/default/lwc/mfeDirtyState/mfeDirtyState.html index 87baae4..59f3697 100644 --- a/force-app/main/default/lwc/mfeDirtyState/mfeDirtyState.html +++ b/force-app/main/default/lwc/mfeDirtyState/mfeDirtyState.html @@ -1,6 +1,6 @@
    - + >
    diff --git a/force-app/main/default/lwc/mfeDirtyState/mfeDirtyState.js b/force-app/main/default/lwc/mfeDirtyState/mfeDirtyState.js index c6d615c..3a16f5a 100644 --- a/force-app/main/default/lwc/mfeDirtyState/mfeDirtyState.js +++ b/force-app/main/default/lwc/mfeDirtyState/mfeDirtyState.js @@ -14,7 +14,7 @@ export default class MfeDirtyState extends LightningElement { return url.toString(); } - // Fires when re-dispatches the wire event on the LWC. + // Fires when re-dispatches the wire event on the LWC. // The event bubbles + composes, so any ancestor of the embedding can listen. handleDirtyState(evt) { const { isDirty, label } = evt.detail ?? {}; diff --git a/force-app/main/default/lwc/mfeReceiveData/mfeReceiveData.html b/force-app/main/default/lwc/mfeReceiveData/mfeReceiveData.html index c4b1078..3a561f9 100644 --- a/force-app/main/default/lwc/mfeReceiveData/mfeReceiveData.html +++ b/force-app/main/default/lwc/mfeReceiveData/mfeReceiveData.html @@ -3,7 +3,7 @@

    Type a message and click Send — the host publishes it via the - props binding on <lightning-embedding>. + props binding on <lightning-ui-embedding>. The guest reads it through viewSDK.getUiState().

    @@ -20,13 +20,13 @@ class="slds-m-bottom_medium" >
    - + >
    diff --git a/force-app/main/default/lwc/mfeReceiveData/mfeReceiveData.js b/force-app/main/default/lwc/mfeReceiveData/mfeReceiveData.js index b723d75..0564988 100644 --- a/force-app/main/default/lwc/mfeReceiveData/mfeReceiveData.js +++ b/force-app/main/default/lwc/mfeReceiveData/mfeReceiveData.js @@ -17,7 +17,7 @@ export default class MfeReceiveData extends LightningElement { } handleSend() { - // `props` on is the canonical host → guest channel + // `props` on is the canonical host → guest channel // (ui/notifications/ui-state). Each new object identity schedules a // ui-state-changed flush; the guest sees it as `state.props`. this.payload = { diff --git a/force-app/main/default/lwc/mfeSendEvent/mfeSendEvent.html b/force-app/main/default/lwc/mfeSendEvent/mfeSendEvent.html index 810d0e2..31aaccf 100644 --- a/force-app/main/default/lwc/mfeSendEvent/mfeSendEvent.html +++ b/force-app/main/default/lwc/mfeSendEvent/mfeSendEvent.html @@ -5,7 +5,7 @@ Click an action button in the MFE — the guest calls view.dispatchEvent(new CustomEvent('mfe-action', { detail })). The bridge forwards it over the wire and - <lightning-embedding> re-dispatches it as a + <lightning-ui-embedding> re-dispatches it as a bubbling DOM event this LWC catches.

    @@ -21,12 +21,12 @@
    - + >
    diff --git a/force-app/main/default/lwc/mfeSendEvent/mfeSendEvent.js b/force-app/main/default/lwc/mfeSendEvent/mfeSendEvent.js index aad1245..7ae4359 100644 --- a/force-app/main/default/lwc/mfeSendEvent/mfeSendEvent.js +++ b/force-app/main/default/lwc/mfeSendEvent/mfeSendEvent.js @@ -20,7 +20,7 @@ export default class MfeSendEvent extends LightningElement { if (this._handler) return; // 'mfe-action' contains a hyphen, which LWC's template `on` // binding does not accept. Register imperatively instead — the event - // bubbles + composes out of , so any ancestor + // bubbles + composes out of , so any ancestor // element inside this template catches it. const host = this.refs?.host; if (!host) return; diff --git a/force-app/main/default/lwc/mfeThemeTokens/mfeThemeTokens.html b/force-app/main/default/lwc/mfeThemeTokens/mfeThemeTokens.html index c0ed09b..5dc355f 100644 --- a/force-app/main/default/lwc/mfeThemeTokens/mfeThemeTokens.html +++ b/force-app/main/default/lwc/mfeThemeTokens/mfeThemeTokens.html @@ -35,7 +35,7 @@ - + > diff --git a/force-app/main/default/lwc/mfeThemeTokens/mfeThemeTokens.js b/force-app/main/default/lwc/mfeThemeTokens/mfeThemeTokens.js index 5d2e1a7..358a0d4 100644 --- a/force-app/main/default/lwc/mfeThemeTokens/mfeThemeTokens.js +++ b/force-app/main/default/lwc/mfeThemeTokens/mfeThemeTokens.js @@ -2,7 +2,7 @@ import { LightningElement, api, track } from 'lwc'; /** * Demo host LWC that pushes host-owned theme tokens into an embedded iframe - * via CSS custom properties on . + * via CSS custom properties on . * * The bridge's `ui-state.styles.variables` channel mirrors CSS custom * properties (`--*`) from the host element's inline style down to the guest. @@ -39,7 +39,7 @@ export default class ThemeTokenEmbed extends LightningElement { } // Tokens are published on TWO bridge channels so the guest reflects - // changes regardless of the deployed version: + // changes regardless of the deployed version: // // 1. `style={themeStyle}` — inline CSS custom properties. Newer base // components forward these as `state.styles.variables`. From 6059845ab58cd519f0c5aa9fb4f0c3a20bddca97 Mon Sep 17 00:00:00 2001 From: AMAN SINGH Date: Tue, 21 Jul 2026 22:22:36 +0530 Subject: [PATCH 10/10] chore(mfe): drop lwc-shell/mfe-app scaffolding from #39, keep lightning-embedding architecture --- .../MfeAppLocalhost.cspTrustedSite-meta.xml | 15 - .../lwc/vendorLwcShell/vendorLwcShell.js | 627 ----- .../vendorLwcShell/vendorLwcShell.js-meta.xml | 5 - mfe-app/index.html | 12 - mfe-app/package-lock.json | 2186 ----------------- mfe-app/package.json | 32 - mfe-app/src/App.tsx | 49 - mfe-app/src/index.css | 126 - mfe-app/src/main.tsx | 10 - mfe-app/src/recipes/AutoResize.tsx | 105 - mfe-app/src/recipes/BasicEmbed.tsx | 54 - mfe-app/src/recipes/DirtyState.tsx | 158 -- mfe-app/src/recipes/ReceiveData.tsx | 84 - mfe-app/src/recipes/SendEvent.tsx | 98 - mfe-app/src/recipes/ThemeTokens.tsx | 100 - mfe-app/tsconfig.app.json | 23 - mfe-app/tsconfig.json | 7 - mfe-app/tsconfig.node.json | 11 - mfe-app/vite.config.ts | 25 - 19 files changed, 3727 deletions(-) delete mode 100644 force-app/main/default/cspTrustedSites/MfeAppLocalhost.cspTrustedSite-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 delete mode 100644 mfe-app/index.html delete mode 100644 mfe-app/package-lock.json delete mode 100644 mfe-app/package.json delete mode 100644 mfe-app/src/App.tsx delete mode 100644 mfe-app/src/index.css delete mode 100644 mfe-app/src/main.tsx delete mode 100644 mfe-app/src/recipes/AutoResize.tsx delete mode 100644 mfe-app/src/recipes/BasicEmbed.tsx delete mode 100644 mfe-app/src/recipes/DirtyState.tsx delete mode 100644 mfe-app/src/recipes/ReceiveData.tsx delete mode 100644 mfe-app/src/recipes/SendEvent.tsx delete mode 100644 mfe-app/src/recipes/ThemeTokens.tsx delete mode 100644 mfe-app/tsconfig.app.json delete mode 100644 mfe-app/tsconfig.json delete mode 100644 mfe-app/tsconfig.node.json delete mode 100644 mfe-app/vite.config.ts diff --git a/force-app/main/default/cspTrustedSites/MfeAppLocalhost.cspTrustedSite-meta.xml b/force-app/main/default/cspTrustedSites/MfeAppLocalhost.cspTrustedSite-meta.xml deleted file mode 100644 index 0987ccd..0000000 --- a/force-app/main/default/cspTrustedSites/MfeAppLocalhost.cspTrustedSite-meta.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - false - false - All - Local MFE app dev server — allows the lwc-shell iframe to load from localhost:4300 - http://localhost:4300 - true - false - false - true - false - false - false - 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 5a8bc9e..0000000 --- a/force-app/main/default/lwc/vendorLwcShell/vendorLwcShell.js +++ /dev/null @@ -1,627 +0,0 @@ -/* eslint-disable */ -// Vendored from @salesforce/experimental-mfe-lwc-shell npm package -/*! @salesforce/experimental-mfe-lwc-shell v2.2.1-rc.8 (2026-06-18) */ -/** - * 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); -} - -/** - * 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 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; - static get observedAttributes() { - return ["src", "srcdoc", "sandbox", "title", "view", "debug"]; - } - constructor() { - super(); - this._shadow = this.attachShadow({ mode: "closed" }); - } - connectedCallback() { - this._renderInitial(); - this._setupMessageListener(); - this._log("connectedCallback"); - } - disconnectedCallback() { - window.removeEventListener("message", this._handleMessage); - 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); - } - 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-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._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/mfe-app/index.html b/mfe-app/index.html deleted file mode 100644 index 6f6a2d9..0000000 --- a/mfe-app/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - MFE App - - -
    - - - diff --git a/mfe-app/package-lock.json b/mfe-app/package-lock.json deleted file mode 100644 index a76c066..0000000 --- a/mfe-app/package-lock.json +++ /dev/null @@ -1,2186 +0,0 @@ -{ - "name": "mfe-app", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "mfe-app", - "version": "1.0.0", - "dependencies": { - "@salesforce/experimental-mfe-bridge": "2.2.1-rc.8", - "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/@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/@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/experimental-mfe-bridge": { - "version": "2.2.1-rc.8", - "resolved": "https://nexus-proxy.repo.local.sfdc.net/nexus/content/groups/npm-all/@salesforce/experimental-mfe-bridge/-/experimental-mfe-bridge-2.2.1-rc.8.tgz", - "integrity": "sha512-uwi/EA9Jt+4u1i69mpOVGoxuRTzqK3mnP49KHxKL2TBic7XTJ1264pmDl/XND+Nzf9nsfNX+awndFasGYAZ+iQ==", - "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", - "dev": true, - "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/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/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/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/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/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", - "dev": true, - "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/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 deleted file mode 100644 index 0ab7cec..0000000 --- a/mfe-app/package.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "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.8", - "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 deleted file mode 100644 index 9278426..0000000 --- a/mfe-app/src/App.tsx +++ /dev/null @@ -1,49 +0,0 @@ -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'; - -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
    • -
    -
    -
    - ); -} - -function App() { - return ( - - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - - ); -} - -export default App; diff --git a/mfe-app/src/index.css b/mfe-app/src/index.css deleted file mode 100644 index ba350a5..0000000 --- a/mfe-app/src/index.css +++ /dev/null @@ -1,126 +0,0 @@ -: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 deleted file mode 100644 index 7825095..0000000 --- a/mfe-app/src/main.tsx +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index 2cef632..0000000 --- a/mfe-app/src/recipes/AutoResize.tsx +++ /dev/null @@ -1,105 +0,0 @@ -/** - * 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 { useEffect, useState } from 'react'; -import bridge from '@salesforce/experimental-mfe-bridge'; - -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()]); - const [connected, setConnected] = useState(bridge.isConnected()); - - useEffect(() => { - // 'connected' fires once after the host's salesforce-shell-ready - // arrives — auto-resize only takes effect once the bridge is connected. - const sync = () => setConnected(bridge.isConnected()); - sync(); - bridge.addEventListener('connected', sync); - return () => bridge.removeEventListener('connected', sync); - }, []); - - 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. -

    - -
    - - -
    - -
    -

    {items.length} item{items.length !== 1 ? 's' : ''}

    - {items.length === 0 ? ( -

    No items — iframe should be at minimum height.

    - ) : ( -
      - {items.map(item => ( -
    • - {item.text} - -
    • - ))} -
    - )} -
    -
    - ); -} diff --git a/mfe-app/src/recipes/BasicEmbed.tsx b/mfe-app/src/recipes/BasicEmbed.tsx deleted file mode 100644 index 9a20205..0000000 --- a/mfe-app/src/recipes/BasicEmbed.tsx +++ /dev/null @@ -1,54 +0,0 @@ -/** - * 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(() => { - // The bridge's 'connected' event fires after the host posts - // `salesforce-shell-ready`. Subscribe — and sync once now in case the - // round-trip already completed before this component mounted. - const sync = () => setConnected(bridge.isConnected()); - sync(); - bridge.addEventListener('connected', sync); - return () => bridge.removeEventListener('connected', sync); - }, []); - - 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 deleted file mode 100644 index dc5fce1..0000000 --- a/mfe-app/src/recipes/DirtyState.tsx +++ /dev/null @@ -1,158 +0,0 @@ -/** - * 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); - const [connected, setConnected] = useState(bridge.isConnected()); - - useEffect(() => { - // 'connected' fires once after the host's salesforce-shell-ready arrives. - const sync = () => setConnected(bridge.isConnected()); - sync(); - bridge.addEventListener('connected', sync); - return () => bridge.removeEventListener('connected', sync); - }, []); - - useEffect(() => { - const dirty = isDirtyCheck(form, saved); - setIsDirty(dirty); - - // Notify the Salesforce host whenever dirty state changes. The bridge - // silently drops dispatchEvent calls made before the host completes its - // handshake, so wait for `connected` if it isn't ready yet. - function send() { - bridge.dispatchEvent( - new CustomEvent('trackdirtystate', { - detail: { - isDirty: dirty, - // instanceId lets the host disambiguate which embedded - // shell is dirty when several are mounted on one page. - instanceId: bridge.instanceId, - label: dirty ? 'Loan application has unsaved changes' : '', - }, - }), - ); - } - - if (bridge.isConnected()) { - send(); - return; - } - bridge.addEventListener('connected', send, { once: true }); - return () => bridge.removeEventListener('connected', send); - }, [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.
    - )} - -
    - - - -
    - -
    - - -
    -
    - ); -} diff --git a/mfe-app/src/recipes/ReceiveData.tsx b/mfe-app/src/recipes/ReceiveData.tsx deleted file mode 100644 index b5812db..0000000 --- a/mfe-app/src/recipes/ReceiveData.tsx +++ /dev/null @@ -1,84 +0,0 @@ -/** - * 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); - const [connected, setConnected] = useState(bridge.isConnected()); - - useEffect(() => { - const handleData = (e: Event) => { - const detail = (e as CustomEvent).detail ?? {}; - setHostData(detail); - setUpdateCount(c => c + 1); - }; - const handleConnected = () => setConnected(bridge.isConnected()); - - // 'data' fires whenever the host calls shell.updateData(payload) - bridge.addEventListener('data', handleData); - // 'connected' fires once the bridge handshake completes — re-render - // so the "running standalone" banner clears when we're embedded. - bridge.addEventListener('connected', handleConnected); - handleConnected(); - return () => { - bridge.removeEventListener('data', handleData); - bridge.removeEventListener('connected', handleConnected); - }; - }, []); - - 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. -

    - -
    -

    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 deleted file mode 100644 index d9996db..0000000 --- a/mfe-app/src/recipes/SendEvent.tsx +++ /dev/null @@ -1,98 +0,0 @@ -/** - * 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 { useEffect, 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([]); - const [connected, setConnected] = useState(bridge.isConnected()); - - useEffect(() => { - const sync = () => setConnected(bridge.isConnected()); - sync(); - bridge.addEventListener('connected', sync); - return () => bridge.removeEventListener('connected', sync); - }, []); - - 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. -

    - -
    -

    Actions

    -
    - {ACTIONS.map(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 deleted file mode 100644 index eee5bf1..0000000 --- a/mfe-app/src/recipes/ThemeTokens.tsx +++ /dev/null @@ -1,100 +0,0 @@ -/** - * 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); - const [connected, setConnected] = useState(bridge.isConnected()); - - 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); - }; - const handleConnected = () => setConnected(bridge.isConnected()); - - // 'theme' fires on connect and whenever the host calls shell.refreshTheme() - bridge.addEventListener('theme', handleTheme); - // 'connected' fires once after the host's salesforce-shell-ready arrives; - // subscribe so the "running standalone" banner clears post-handshake. - bridge.addEventListener('connected', handleConnected); - handleConnected(); - return () => { - bridge.removeEventListener('theme', handleTheme); - bridge.removeEventListener('connected', handleConnected); - }; - }, []); - - 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. -

    - -
    -

    Syncs received: {syncCount}

    -

    Tokens (first 20 of {Object.keys(tokens).length})

    - - {tokenEntries.length === 0 ? ( -

    Waiting for theme data…

    - ) : ( - - - - - - - - - {tokenEntries.map(([prop, value]) => ( - - - - - ))} - -
    PropertyValue
    {prop}{value}
    - )} -
    -
    - ); -} diff --git a/mfe-app/tsconfig.app.json b/mfe-app/tsconfig.app.json deleted file mode 100644 index aeff964..0000000 --- a/mfe-app/tsconfig.app.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "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 deleted file mode 100644 index f6df6c7..0000000 --- a/mfe-app/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "files": [], - "references": [ - { "path": "./tsconfig.app.json" }, - { "path": "./tsconfig.node.json" } - ] -} diff --git a/mfe-app/tsconfig.node.json b/mfe-app/tsconfig.node.json deleted file mode 100644 index 8cdf5e8..0000000 --- a/mfe-app/tsconfig.node.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "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 deleted file mode 100644 index 84a9d93..0000000 --- a/mfe-app/vite.config.ts +++ /dev/null @@ -1,25 +0,0 @@ -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', - }, -});