Skip to content

Commit 2f1d97a

Browse files
aman06itcharlesw-salesforcedependabot[bot]
authored
Recipes for GA (#41)
* feat: add multi-framework recipes IA, MFE recipes, and embedding scaffolding 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. * planning doc removed * feat(embedding): migrate to Platform SDK + <lightning-embedding> Replaces the legacy @salesforce/experimental-mfe-bridge / vendored <lwc-shell> stack with the GA-track @salesforce/platform-sdk and the <lightning-embedding> 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 <lightning-embedding> wrapper; drop imperative document.createElement('lwc-shell') and c/vendorLwcShell imports - Delete vendored vendorLwcShell (no longer needed; <lightning-embedding> 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 * build(deps): bump @salesforce/platform-sdk, ui-bundle, and vite-plugin-ui-bundle to ^10.24.0 (#40) * updated ga recipes * fix(mfe): align recipes with sf-embedding-bridge channels - ReceiveData: host publishes via props binding; guest reads getUiState (drop URL query-param remount) - ThemeTokens: publish tokens on both style variables and props channels for base-component version resilience; guest prefers styles.variables, falls back to props - AutoResize: rely on bootstrap's auto-attached EmbeddingResizer; remove fixed-height cap so iframe grows with content - bump @salesforce/platform-sdk to ^11.27.0 - ignore *.tsbuildinfo * mfe-app moved under react-recipes * build(deps): bump websocket-driver Bumps [websocket-driver](https://github.com/faye/websocket-driver-node) from 0.7.4 to 0.7.5. - [Changelog](https://github.com/faye/websocket-driver-node/blob/main/CHANGELOG.md) - [Commits](faye/websocket-driver-node@0.7.4...0.7.5) --- updated-dependencies: - dependency-name: websocket-driver dependency-version: 0.7.5 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> * refactor(mfe): rename lightning-embedding to lightning-ui-embedding * chore(mfe): drop lwc-shell/mfe-app scaffolding from #39, keep lightning-embedding architecture --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Charles Watkins <168678941+charlesw-salesforce@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 parent 048a30b commit 2f1d97a

112 files changed

Lines changed: 2014 additions & 4041 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ deploy-options.json
1212
# LWC VSCode autocomplete
1313
jsconfig.json
1414

15+
#dist
16+
dist/
17+
18+
# TypeScript incremental build cache
19+
*.tsbuildinfo
20+
1521
# LWC Jest coverage reports
1622
coverage/
1723

README.md

Lines changed: 22 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -40,27 +40,23 @@ Today this mode lives in [`force-app/main/react-recipes/uiBundles/reactRecipes/`
4040

4141
### Externally Hosted
4242

43-
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.
43+
The framework app runs on your own infrastructure (Vercel, AWS, anywhere) and is embedded into a Salesforce Lightning page via the standard `<lightning-embedding>` base component. The Platform SDK is the contract on the guest side; the sf-embedding protocol is the wire format underneath.
4444

4545
```mermaid
4646
graph LR
47-
A[External Framework App<br/>mfe-app/] -->|iframe src| B[lwc-shell]
47+
A[Guest recipes<br/>reactRecipes /embedding/*] -->|iframe src| B[lightning-embedding]
4848
B -->|embedded in| C[LWC Host Component]
4949
C -->|deployed to| D[Salesforce Org]
50-
B <-->|postMessage bridge| A
51-
D -->|updateData / events| B
50+
A <-->|"@salesforce/platform-sdk"| B
5251
```
5352

5453
**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.
5554

56-
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).
55+
Today this mode lives in the React Recipes bundle under [`force-app/main/react-recipes/uiBundles/reactRecipes/src/mfe/`](force-app/main/react-recipes/uiBundles/reactRecipes/src/mfe/) (the guest recipes, served on `/embedding/*`) plus [`force-app/main/default/lwc/mfe*`](force-app/main/default/lwc/) (the LWC host components).
5756

58-
### How the framework app talks to Salesforce
57+
### The Platform SDK: one API, both modes
5958

60-
The two hosting modes use different APIs because they run in different security contexts:
61-
62-
- **Salesforce-hosted** recipes call native Lightning modules directly — `lightning/uiRecordApi`, `lightning/navigation`, `lightning/graphql`, `@salesforce/apex` — same as any LWC.
63-
- **Externally hosted** recipes use the postMessage bridge ([`@salesforce/experimental-mfe-bridge`](https://www.npmjs.com/package/@salesforce/experimental-mfe-bridge)) to talk to the host LWC, which then calls Lightning modules on the MFE's behalf. The bridge surface (`bridge.isConnected()`, `bridge.dispatchEvent()`, `bridge.addEventListener('data', …)`, etc.) is the contract between the embedded app and the host shell.
59+
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.
6460

6561
## Table of Contents
6662

@@ -244,56 +240,43 @@ These recipes run as a Salesforce UI Bundle served directly from the org. Today
244240

245241
## Install & Run Externally Hosted Recipes
246242

247-
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.
243+
These recipes serve the guest app on your own server and embed it into Salesforce via the standard `<lightning-embedding>` base component. The guest recipes live inside the React Recipes bundle (`src/mfe/recipes/`) and are served on the `/embedding/*` routes of its dev server. In development, "externally hosted" means `localhost:5173`; in production you would point the LWC host at your deployed URL. Today the only implementation is React; Vue and Angular are planned.
248244

249-
> **Before you start:** complete the [Scratch Org](#setting-up-a-scratch-org) or [Sandbox](#setting-up-a-sandbox) setup first. Those steps deploy the shared metadata (objects, classes, **CSP Trusted Site for `localhost:4300`**), assign the `recipes` permission set, and import the sample Contact data the recipes display. Without them the iframe is blocked or shows empty data.
245+
> **Note:** The CSP trusted site for `localhost:5173` is included in the shared metadata deployed in the org setup steps above. No additional metadata deploy is needed.
250246
251-
1. Install dependencies for the external app:
247+
1. Install dependencies and start the React Recipes dev server:
252248

253249
```bash
254-
cd mfe-app
250+
cd force-app/main/react-recipes/uiBundles/reactRecipes
255251
npm install
256-
```
257-
258-
1. Start the external app's dev server:
259-
260-
```bash
261252
npm run dev
262253
```
263254

264-
The app starts at `http://localhost:4300`. Keep this running while using the externally hosted recipes in your org.
255+
The server starts at `http://localhost:5173`; the guest recipes are served under `/embedding/*` (e.g. `http://localhost:5173/embedding/basic-embed`). Keep this running while using the externally hosted recipes in your org.
265256

266257
1. Deploy the LWC host components:
267258

268259
```bash
269-
cd ..
270-
sf project deploy start --source-dir force-app/main/default/lwc
271-
```
272-
273-
This deploys both the `mfe*` recipe wrappers and `vendorLwcShell`, which registers the `<lwc-shell>` custom element they all rely on. `vendorLwcShell` is a vendored bundle of [`@salesforce/experimental-mfe-lwc-shell`](https://www.npmjs.com/package/@salesforce/experimental-mfe-lwc-shell) — it's checked into the repo, so you don't need to build it yourself. Refresh from npm when a new version ships.
274-
275-
1. Add a host component to a Lightning page:
276-
277-
```bash
278-
sf org open
260+
cd ../../../../..
261+
sf project deploy start -d force-app/main/default/lwc
279262
```
280263

281-
In the org, go to **Setup → Lightning App Builder → New → App Page**, drag a *Custom* component (e.g. `mfeBasicEmbed`, `mfeReceiveData`) from the left panel onto the canvas, then **Save → Activate** and pick the apps where it should appear. Open the page from App Launcher to use the recipe.
264+
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.
282265

283266
### Available externally hosted recipes
284267

285-
| LWC host component | External app route | What it demonstrates |
268+
| LWC host component | Guest route | What it demonstrates |
286269
|---|---|---|
287-
| `mfeBasicEmbed` | `/basic-embed` | Minimum viable embed — bridge connection detection |
288-
| `mfeReceiveData` | `/receive-data` | Host pushes data into guest via `shell.updateData()` |
289-
| `mfeSendEvent` | `/send-event` | Guest dispatches events to host via `bridge.dispatchEvent()` |
290-
| `mfeAutoResize` | `/auto-resize` | iframe height follows guest content via ResizeObserver |
291-
| `mfeThemeTokens` | `/theme-tokens` | Salesforce CSS custom properties synced to guest |
292-
| `mfeDirtyState` | `/dirty-state` | Guest notifies host of unsaved changes |
270+
| `mfeBasicEmbed` | `/embedding/basic-embed` | Minimum viable embed using `<lightning-embedding>``chatSDK.getHostContext()` for connection detection |
271+
| `mfeReceiveData` | `/embedding/receive-data` | Host re-mounts `<lightning-embedding>` with new src carrying URL query params; guest reads via `URLSearchParams` and `viewSDK.getUiProps()` |
272+
| `mfeSendEvent` | `/embedding/send-event` | Guest calls `viewSDK.dispatchEvent(name, data)`; surface-level routing is host-runtime specific |
273+
| `mfeAutoResize` | `/embedding/auto-resize` | Guest tracks body height with ResizeObserver and calls `viewSDK.resize()` |
274+
| `mfeThemeTokens` | `/embedding/theme-tokens` | Guest reads host theme via `viewSDK.getTheme()` and the broader environment via `chatSDK.getHostContext()` |
275+
| `mfeDirtyState` | `/embedding/dirty-state` | Guest calls `viewSDK.markDirtyState()` / `clearDirtyState()` to signal unsaved changes |
293276

294277
### Pointing at a deployed external app
295278

296-
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`.
279+
For production or sandbox use, deploy the guest app to a hosted URL and set the `baseUrl` property on each LWC host component to point at that URL instead of `localhost:5173`.
297280

298281
## Local Development
299282

force-app/main/default/cspTrustedSites/MfeAppLocalhost.cspTrustedSite-meta.xml renamed to force-app/main/default/cspTrustedSites/ReactRecipesLocalhost.cspTrustedSite-meta.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
<canAccessCamera>false</canAccessCamera>
44
<canAccessMicrophone>false</canAccessMicrophone>
55
<context>All</context>
6-
<description>Local MFE app dev server — allows the lwc-shell iframe to load from localhost:4300</description>
7-
<endpointUrl>http://localhost:4300</endpointUrl>
6+
<description>React Recipes dev server — allows the lightning-embedding iframe to load externally-hosted MFE recipe routes from localhost:5173</description>
7+
<endpointUrl>http://localhost:5173</endpointUrl>
88
<isActive>true</isActive>
99
<isApplicableToConnectSrc>false</isApplicableToConnectSrc>
1010
<isApplicableToFontSrc>false</isApplicableToFontSrc>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/* Override lightning-ui-embedding's default :host { height: 100% }. The bridge
2+
* sets an explicit height in px on the inner iframe when the guest sends
3+
* ui/notifications/resize; letting the host element flow means the LWC
4+
* grows/shrinks to that height instead of clipping/scrolling inside a
5+
* fixed-height container. */
6+
.auto-resize-embed {
7+
display: block;
8+
height: auto;
9+
min-height: 120px;
10+
}

force-app/main/default/lwc/mfeAutoResize/mfeAutoResize.html

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,21 @@
22
<lightning-card title="MFE Auto-Resize" icon-name="custom:custom59">
33
<div class="slds-p-around_medium">
44
<p class="slds-text-body_regular slds-m-bottom_medium">
5-
Add or remove items in the MFE — the iframe height adjusts automatically.
6-
No fixed height is set. The bridge reports content height changes via a
7-
ResizeObserver.
5+
Add or remove items in the MFE — the guest's bootstrap-attached
6+
<code>EmbeddingResizer</code> sends
7+
<code>ui/notifications/resize</code> and
8+
<code>&lt;lightning-ui-embedding&gt;</code> applies the height to
9+
its inner iframe.
810
</p>
9-
<div class="shell-container" lwc:dom="manual"></div>
11+
<div class="shell-container">
12+
<lightning-ui-embedding
13+
class="auto-resize-embed"
14+
sandbox="allow-forms allow-modals"
15+
shell-title="MFE Auto-Resize"
16+
debug={debug}
17+
src={computedSrc}
18+
></lightning-ui-embedding>
19+
</div>
1020
</div>
1121
</lightning-card>
1222
</template>
Lines changed: 3 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,12 @@
11
import { LightningElement, api } from 'lwc';
2-
import 'c/vendorLwcShell';
32

43
export default class MfeAutoResize extends LightningElement {
5-
@api baseUrl = 'http://localhost:4300';
6-
_shellElement;
4+
@api baseUrl = 'http://localhost:5173';
5+
debug = true;
76

87
get computedSrc() {
98
const url = new URL(this.baseUrl);
10-
url.pathname = '/auto-resize';
9+
url.pathname = '/embedding/auto-resize';
1110
return url.toString();
1211
}
13-
14-
renderedCallback() {
15-
this._initializeShell();
16-
}
17-
18-
disconnectedCallback() {
19-
this._shellElement = null;
20-
}
21-
22-
_initializeShell() {
23-
if (this._shellElement) {
24-
return;
25-
}
26-
const container = this.template.querySelector('.shell-container');
27-
if (!container) {
28-
return;
29-
}
30-
const shell = document.createElement('lwc-shell');
31-
shell.sandbox = 'allow-forms allow-modals';
32-
shell.title = 'MFE Auto-Resize';
33-
shell.src = this.computedSrc;
34-
35-
// No explicit resize handler needed — lwc-shell receives 'resize' events
36-
// from the bridge and updates iframe.style.height automatically.
37-
// Cancel the event here if you need a fixed height instead.
38-
shell.addEventListener('widget-ready', () => {
39-
// eslint-disable-next-line no-console
40-
console.log('[MfeAutoResize] widget-ready');
41-
});
42-
43-
container.appendChild(shell);
44-
this._shellElement = shell;
45-
}
4612
}

force-app/main/default/lwc/mfeBasicEmbed/mfeBasicEmbed.html

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,17 @@
22
<lightning-card title="MFE Basic Embed" icon-name="custom:custom59">
33
<div class="slds-p-around_medium">
44
<p class="slds-text-body_regular slds-m-bottom_medium">
5-
Minimum viable embed. Creates an <code>lwc-shell</code> pointing at the
6-
external MFE app and handles the <code>widget-ready</code> event.
5+
Minimum viable embed using the standard
6+
<code>&lt;lightning-ui-embedding&gt;</code> base component.
77
</p>
8-
<div class="shell-container" lwc:dom="manual"></div>
8+
<div class="shell-container" style="height: 400px">
9+
<lightning-ui-embedding
10+
sandbox="allow-forms allow-modals"
11+
shell-title="MFE Basic Embed"
12+
debug={debug}
13+
src={computedSrc}
14+
></lightning-ui-embedding>
15+
</div>
916
</div>
1017
</lightning-card>
1118
</template>
Lines changed: 3 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,12 @@
11
import { LightningElement, api } from 'lwc';
2-
import 'c/vendorLwcShell';
32

43
export default class MfeBasicEmbed extends LightningElement {
5-
@api baseUrl = 'http://localhost:4300';
6-
_shellElement;
4+
@api baseUrl = 'http://localhost:5173';
5+
debug = true;
76

87
get computedSrc() {
98
const url = new URL(this.baseUrl);
10-
url.pathname = '/basic-embed';
9+
url.pathname = '/embedding/basic-embed';
1110
return url.toString();
1211
}
13-
14-
renderedCallback() {
15-
this._initializeShell();
16-
}
17-
18-
disconnectedCallback() {
19-
this._shellElement = null;
20-
}
21-
22-
_initializeShell() {
23-
if (this._shellElement) {
24-
return;
25-
}
26-
const container = this.template.querySelector('.shell-container');
27-
if (!container) {
28-
return;
29-
}
30-
const shell = document.createElement('lwc-shell');
31-
shell.sandbox = 'allow-forms allow-modals';
32-
shell.title = 'MFE Basic Embed';
33-
shell.src = this.computedSrc;
34-
shell.addEventListener('widget-ready', () => {
35-
// eslint-disable-next-line no-console
36-
console.log('[MfeBasicEmbed] widget-ready');
37-
});
38-
container.appendChild(shell);
39-
this._shellElement = shell;
40-
}
4112
}
Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,32 @@
11
<template>
22
<lightning-card title="MFE Dirty State" icon-name="custom:custom59">
3-
<div class="slds-p-around_medium">
3+
<div class="slds-p-around_medium" ontrackdirtystate={handleDirtyState}>
44
<p class="slds-text-body_regular slds-m-bottom_medium">
5-
Type into any field in the MFE form. This component receives a
6-
<code>trackdirtystate</code> event, re-dispatches it to Lightning
7-
Experience (which blocks tab navigation), and attaches a
8-
<code>beforeunload</code> handler so the browser also warns on
9-
close or reload.
5+
Edit the MFE form — the guest calls
6+
<code>viewSDK.markDirtyState()</code> on every change and
7+
<code>viewSDK.clearDirtyState()</code> after save. The host
8+
surface decides how to honour the dirty signal (e.g. block
9+
navigation).
1010
</p>
11-
<div class={warningClass} role="alert">
12-
<span>&#9888; {dirtyLabel} — save or discard before leaving.</span>
11+
12+
<template lwc:if={hasSignal}>
13+
<div if:true={isDirty} class="slds-notify slds-notify_alert slds-theme_alert-texture slds-theme_warning slds-m-bottom_medium">
14+
Host received <code>trackdirtystate</code> — MFE is dirty
15+
<template lwc:if={label}>&nbsp;({label})</template>.
16+
</div>
17+
<div if:false={isDirty} class="slds-notify slds-notify_alert slds-theme_alert-texture slds-theme_success slds-m-bottom_medium">
18+
Host received <code>trackdirtystate</code> — MFE is clean.
19+
</div>
20+
</template>
21+
22+
<div class="shell-container" style="height: 400px">
23+
<lightning-ui-embedding
24+
sandbox="allow-forms allow-modals"
25+
shell-title="MFE Dirty State"
26+
debug={debug}
27+
src={computedSrc}
28+
></lightning-ui-embedding>
1329
</div>
14-
<div class="shell-container" lwc:dom="manual"></div>
1530
</div>
1631
</lightning-card>
1732
</template>

0 commit comments

Comments
 (0)