@salesforce/vite-plugin-lwc-ui-bundle 11.13.0 → 11.13.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,6 +4,9 @@ Vite plugin for compiling and running LWC components off-platform. Bundles the f
4
4
 
5
5
  > **Getting started?** See the [Consumer Guide](docs/consumer-guide.md) for a
6
6
  > step-by-step walkthrough of adding this plugin to an existing LWC project.
7
+ >
8
+ > - **Porting a platform component?** See the [Migration Guide](docs/migration-guide.md).
9
+ > - **What's not supported off-platform?** See [Limitations & Unsupported Features](docs/limitations.md).
7
10
 
8
11
  ## Installation
9
12
 
@@ -515,6 +515,9 @@ export const someExport = () => {};
515
515
  lwcVitePlugin({ stubs: { "force/someModule": "./stubs/someModule.js" } });
516
516
  ```
517
517
 
518
+ See [Limitations → Not supported](limitations.md#not-supported--and-how-to-work-around-it)
519
+ for stubbing navigation, LMS, Apex, and other core-only modules.
520
+
518
521
  ### Component renders but looks unstyled
519
522
 
520
523
  Add SLDS import to `bootstrap.js`:
@@ -527,6 +530,8 @@ import "@salesforce-ux/design-system/assets/styles/salesforce-lightning-design-s
527
530
 
528
531
  ## Reference
529
532
 
533
+ - **Migration Guide:** [migration-guide.md](migration-guide.md) — porting a platform LWC to a bundle
534
+ - **Limitations & Unsupported Features:** [limitations.md](limitations.md) — what's not supported off-platform, and the workaround for each gap
530
535
  - **Live Preview & HMR verification:** [live-preview-hmr-verification.md](live-preview-hmr-verification.md) — how local dev (Live Preview, HMR, dev-gateway) works and is verified
531
536
  - **npm:** https://www.npmjs.com/package/@salesforce/vite-plugin-lwc-ui-bundle
532
537
  - **Source:** https://github.com/salesforce-experience-platform-emu/webapps/tree/main/packages/vite-plugin-lwc-ui-bundle
@@ -0,0 +1,365 @@
1
+ # LWC UI Bundle Limitations & Unsupported Features
2
+
3
+ An LWC UI Bundle compiles your Lightning Web Components off-platform into a static
4
+ `dist/` — an `index.html` plus hashed `assets/*.js`/`*.css` by default, or a single
5
+ inlined `dist/index.html` if you add [`vite-plugin-singlefile`](https://www.npmjs.com/package/vite-plugin-singlefile)
6
+ (see [Consumer Guide → Off-Platform Build](consumer-guide.md#off-platform-build)). The
7
+ output runs anywhere with a DOM — an agentic MCP host (ChatGPT, MCP Apps), a plain
8
+ website, or a `*.salesforce.app`-served page. Because there is **no Lightning runtime**
9
+ around your component in any of those surfaces, some platform capabilities that are
10
+ ambient on-platform have no equivalent off-platform. This document is the authoritative
11
+ list of what is and isn't supported, _why_, and the recommended workaround for each gap.
12
+
13
+ > Porting an existing platform component? Pair this reference with the
14
+ > [Migration Guide](migration-guide.md), which walks through applying these
15
+ > workarounds step by step.
16
+
17
+ ---
18
+
19
+ ## The core architectural constraint
20
+
21
+ A platform LWC runs **inside** a Salesforce org: the Lightning runtime mounts it,
22
+ `@salesforce/*` and `lightning/*` modules resolve to live org services, and Lightning
23
+ Data Service (LDS) backs wire adapters with a **reactive client-side store** that
24
+ pushes updates as records change.
25
+
26
+ An LWC UI Bundle runs **anywhere with a DOM** — a browser tab, an MCP host, a
27
+ `*.salesforce.app`-served page. There is no org runtime in the page. The plugin
28
+ recreates the pieces it can as **scoped module providers**: small generated JS modules
29
+ that stand in for `@salesforce/label/*`, `@salesforce/i18n/*`, and friends. Everything
30
+ else — anything that needs a live authenticated session, a reactive store, a shared
31
+ message bus, or platform navigation — either routes through an explicit data path
32
+ (GraphQL / the Data SDK / an MCP tool) or is simply **not available** and must be
33
+ stubbed.
34
+
35
+ Two consequences flow from this and explain almost every limitation below:
36
+
37
+ 1. **No reactive store.** Wire adapters can fetch once, but nothing observes org state
38
+ to push updates. `subscribe()` is a no-op.
39
+ 2. **No ambient authenticated session** unless the bundle is served from a
40
+ `*.salesforce.app` app domain (deployed) or proxied to an org (`lwcProxy`, dev
41
+ only). A raw static file has no org to call.
42
+
43
+ ---
44
+
45
+ ## Support matrix
46
+
47
+ | Capability | Status | Off-platform mechanism |
48
+ | ------------------------------------------------------- | -------------- | ------------------------------------------------------------- |
49
+ | LWC component compilation (`.js/.html/.css`) | ✅ Full | `@lwc/rollup-plugin` |
50
+ | `lightning/*` base components | ✅ Full | `lightning-base-components` (npm) |
51
+ | Custom Labels (`@salesforce/label/*`) | ✅ Provider | `builtins.label()` static · `builtins.labelsGraphql()` live |
52
+ | i18n (`@salesforce/i18n/*`) | ⚠️ Partial | `builtins.i18n()` — live locale identity, static CLDR formats |
53
+ | Client / form factor (`@salesforce/client/*`) | ✅ Provider | `builtins.client()` |
54
+ | Feature gates (`@salesforce/gate/*`) | ⚠️ Static | `builtins.gate()` — **defaults open**, no live gate state |
55
+ | Access checks (`@salesforce/accessCheck/*`) | ⚠️ Static | `builtins.accessCheck()` — **defaults false**, no live perms |
56
+ | `lightning/primitiveUtils` | ✅ Provider | `builtins.primitiveUtils()` |
57
+ | GraphQL (`lightning/graphql`) | ✅ Supported | `builtins.lds()` default registry → MCP `graphqlQuery` tool |
58
+ | LDS wire/imperative adapters | ⚠️ Partial | `builtins.lds()` — **only registered exports**; MCP-backed |
59
+ | REST / Apex REST | ✅ Supported | `createDataSDK().fetch?.()` → `/services/apexrest/*` |
60
+ | Imperative Apex (`@salesforce/apex/*`) | ❌ Unsupported | No provider — use GraphQL or Apex REST |
61
+ | `@AuraEnabled` Apex methods | ❌ Unsupported | Expose as `@RestResource` instead |
62
+ | Lightning Message Service (`lightning/messageService`) | ❌ Unsupported | No provider — stub with `EventTarget` / host bridge |
63
+ | Navigation (`lightning/navigation`, `force/navigation`) | ❌ Unsupported | No provider — stub `NavigationMixin` |
64
+ | LDS store subscriptions / auto-refresh | ❌ Unsupported | `subscribe()` is a no-op; use `refresh()` or re-query |
65
+ | Aura (`aura`) and other `force/*` modules | ❌ Unsupported | No provider — supply a stub |
66
+ | Static Resources (`@salesforce/resourceUrl/*`) | ❌ Unsupported | Bundle assets directly / import as Vite assets |
67
+ | Wire to `$CurrentPageReference`, User, Org context | ❌ Unsupported | No provider — pass context in explicitly |
68
+
69
+ Legend: ✅ works like platform · ⚠️ works with documented differences · ❌ no
70
+ off-platform equivalent, workaround required.
71
+
72
+ ---
73
+
74
+ ## Supported with differences
75
+
76
+ These work, but not identically to the platform. Know the difference before you rely
77
+ on them.
78
+
79
+ ### i18n — live identity, static formats
80
+
81
+ `builtins.i18n()` derives locale, language, currency, and time zone from the browser's
82
+ `Intl` API at runtime, so `@salesforce/i18n/lang` and similar identity values are
83
+ live. **Format patterns** (number/date/currency CLDR patterns), however, use en-US
84
+ defaults rather than the org's locale data. If your component depends on exact
85
+ locale-specific formatting matching the org, format through `Intl.NumberFormat` /
86
+ `Intl.DateTimeFormat` explicitly rather than trusting the static patterns.
87
+
88
+ ### Feature gates — default open, not live
89
+
90
+ `builtins.gate()` resolves every `@salesforce/gate/*` import to **open** by default.
91
+ There is no connection to the org's live gate state. Pass overrides to model closed
92
+ gates: `builtins.gate({ myFeature: false })`. Note that `lightning-base-components`
93
+ use gates internally — you must include `builtins.gate()` or base components throw
94
+ `Cannot read properties of undefined (reading 'isOpen')`.
95
+
96
+ ### Access checks — default false, not live
97
+
98
+ `builtins.accessCheck()` resolves every `@salesforce/accessCheck/*` import to `false`
99
+ by default — no live permission evaluation. Pass overrides for checks your UI depends
100
+ on: `builtins.accessCheck({ MyCustomPerm: true })`. Because the default is `false`,
101
+ permission-gated UI is hidden unless you opt it in; verify your overrides match the
102
+ org behavior you're emulating.
103
+
104
+ > **Coming soon — live permission evaluation.** A GraphQL-backed provider that resolves
105
+ > `@salesforce/accessCheck/*` (plus `@salesforce/userPermission/*` and
106
+ > `@salesforce/customPermission/*`) against the live user's permissions via UI API
107
+ > GraphQL is in flight ([#670](https://github.com/salesforce-experience-platform-emu/webapps/pull/670)).
108
+ > Until it merges, access checks are static-only as described above.
109
+
110
+ ### Custom Labels — static default, or live via GraphQL
111
+
112
+ There are two label providers; pick per your data path:
113
+
114
+ - **`builtins.label()` (static, the default).** Returns label strings from a defaults
115
+ map. Unknown keys get a human-readable fallback derived from the key
116
+ (`c.appTitle` → "App Title"). It does **not** fetch live translations. Provide real
117
+ values via `builtins.label({ "c.appTitle": "My App" })` or copy them from your
118
+ `CustomLabels.labels-meta.xml`. Translations for non-default languages are not
119
+ resolved.
120
+ - **`builtins.labelsGraphql()` (live, opt-in).** Each `@salesforce/label/*` import
121
+ resolves to a runtime module that batches its key and fetches the translated value
122
+ via UI API GraphQL, so labels reflect the **current user's locale**. Static defaults
123
+ (and any `staticOverrides` you pass) act as the pre-fetch fallback. This needs a live
124
+ data path (a real MCP host, a mocked `callTool`, or a `*.salesforce.app`-served
125
+ bundle) — see [Data access and authentication](#data-access-and-authentication). To
126
+ use it, swap `label()` for `labelsGraphql()` in your `providers` array.
127
+
128
+ ### LDS — partial adapter coverage, no store
129
+
130
+ The `lds()` provider (on by default) routes a **registered** set of adapters to MCP
131
+ tools. The default registry covers:
132
+
133
+ | Specifier | Export | Shape |
134
+ | --------------------------- | -------------------------- | ------------------------ |
135
+ | `lightning/uiRecordApi` | `getRecord` | wire |
136
+ | `lightning/uiRecordApi` | `createRecord` | imperative-mutation |
137
+ | `lightning/uiRecordApi` | `updateRecord` | imperative-mutation |
138
+ | `lightning/uiObjectInfoApi` | `getObjectInfo_imperative` | imperative-read (legacy) |
139
+
140
+ Code using only these exports ports unchanged, and each shape delegates to the same
141
+ OneStore invoker used on-platform, so success payloads are deep-frozen and validation
142
+ errors throw the same typed classes. Two hard limits:
143
+
144
+ - **Unregistered exports don't resolve.** `getListUi`, `getRelatedListRecords`,
145
+ `deleteRecord`, and any other LDS export not in the registry pass through unresolved
146
+ and fail the build. Register your own MCP-backed adapter via the `lds({ … })` config
147
+ (see the [plugin README](../README.md#ldsadapters)), or replace the call with
148
+ GraphQL.
149
+ - **`subscribe()` is a deliberate no-op.** Off-platform there is no reactive store to
150
+ observe, so the subscribe callback never fires and the returned unsubscribe is
151
+ idempotent. Data does not auto-update. Use `refresh()` (on the
152
+ `subscribable-refreshable` shape) or re-query when you need fresh data.
153
+
154
+ ---
155
+
156
+ ## Not supported — and how to work around it
157
+
158
+ ### Imperative Apex (`@salesforce/apex/MyClass.myMethod`)
159
+
160
+ **No provider.** There is no way to invoke an `@AuraEnabled` Apex method off-platform.
161
+
162
+ **Workarounds:**
163
+
164
+ 1. **GraphQL** — if the method only reads records, replace it with a UI API GraphQL
165
+ query. This is the preferred path and works both in a `@wire` and imperatively via
166
+ the Data SDK.
167
+ 2. **Apex REST** — for business logic that can't be expressed as GraphQL, expose it as
168
+ an `@RestResource` and call it through the Data SDK:
169
+
170
+ ```js
171
+ import { createDataSDK } from "@salesforce/platform-sdk";
172
+ const sdk = await createDataSDK();
173
+ const res = await sdk.fetch?.("/services/apexrest/property/listings");
174
+ const listings = await res?.json();
175
+ ```
176
+
177
+ Note the distinction: **`@AuraEnabled` methods are not reachable; only `@RestResource`
178
+ endpoints are.** Plan to expose an Apex REST surface for any imperative Apex you can't
179
+ convert to GraphQL.
180
+
181
+ ### Lightning Message Service (`lightning/messageService`)
182
+
183
+ **No provider.** LMS assumes a shared platform message bus (backed by
184
+ `@salesforce/messageChannel/*` metadata) that doesn't exist off-platform.
185
+
186
+ **Workaround — stub it, backed by a local bus:** for cross-component messaging _within
187
+ the same bundle_, a module-scoped `EventTarget` reproduces publish/subscribe
188
+ faithfully. For cross-surface messaging (bundle ↔ host), route through the MCP/host
189
+ bridge instead.
190
+
191
+ ```js
192
+ // src/stubs/message-service.js
193
+ const bus = new EventTarget();
194
+
195
+ export function createMessageContext() {
196
+ return {};
197
+ }
198
+ export function releaseMessageContext() {}
199
+ export function publish(_ctx, channel, message) {
200
+ bus.dispatchEvent(new CustomEvent(channelKey(channel), { detail: message }));
201
+ }
202
+ export function subscribe(_ctx, channel, listener) {
203
+ const handler = (e) => listener(e.detail);
204
+ const key = channelKey(channel);
205
+ bus.addEventListener(key, handler);
206
+ return { key, handler };
207
+ }
208
+ export function unsubscribe(sub) {
209
+ if (sub) bus.removeEventListener(sub.key, sub.handler);
210
+ }
211
+ export const APPLICATION_SCOPE = Symbol("APPLICATION_SCOPE");
212
+ export const MessageContext = Symbol("MessageContext");
213
+
214
+ function channelKey(channel) {
215
+ return typeof channel === "string" ? channel : String(channel?.name ?? channel);
216
+ }
217
+ ```
218
+
219
+ ```js
220
+ // vite.config.js
221
+ lwcVitePlugin({ stubs: { "lightning/messageService": "src/stubs/message-service.js" } });
222
+ ```
223
+
224
+ `@salesforce/messageChannel/*` imports (the channel references) also have no provider —
225
+ stub each to a plain identifier string, or import the channel name as a constant.
226
+
227
+ ### Navigation (`lightning/navigation`, `force/navigation`)
228
+
229
+ **No provider.** There is no page reference resolver or router off-platform.
230
+
231
+ **Workaround — a no-op `NavigationMixin` stub**, then decide per call site what
232
+ navigation should mean (a real `window.location` change, a host callback, or nothing).
233
+ This is exactly what the [`lwc-records` example](../../../examples/lwc-axl/lwc-records/sf/lwc/force/navigation/navigation.js)
234
+ ships:
235
+
236
+ ```js
237
+ // src/stubs/navigation.js
238
+ export const CurrentPageReference = { adapter: Symbol("CurrentPageReference") };
239
+
240
+ export const NavigationMixin = (Base) =>
241
+ class extends Base {
242
+ [NavigationMixin.Navigate]() {} // no-op, or window.location / host callback
243
+ [NavigationMixin.GenerateUrl]() {
244
+ return Promise.resolve("");
245
+ }
246
+ };
247
+ NavigationMixin.Navigate = Symbol("Navigate");
248
+ NavigationMixin.GenerateUrl = Symbol("GenerateUrl");
249
+ ```
250
+
251
+ ```js
252
+ // vite.config.js
253
+ lwcVitePlugin({ stubs: { "lightning/navigation": "src/stubs/navigation.js" } });
254
+ ```
255
+
256
+ ### Aura and other `force/*` / core-only modules
257
+
258
+ **No provider.** `aura`, `logger`, and `force/*` / `runtime_*` modules are core-only.
259
+ If the build fails with `Rollup failed to resolve import "force/someModule"`, add a
260
+ stub exporting the shapes your code imports. The `lwc-records` example stubs several:
261
+
262
+ ```js
263
+ // vite.config.js
264
+ lwcVitePlugin({
265
+ stubs: {
266
+ aura: "src/stubs/aura-off-platform.js",
267
+ logger: "src/stubs/logger-stub.js",
268
+ "force/someModule": "src/stubs/some-module.js",
269
+ },
270
+ });
271
+ ```
272
+
273
+ A stub only needs to export the named bindings your components import — often no-ops or
274
+ empty objects are enough to satisfy the bundler and let the rest of the app run.
275
+
276
+ ### Static Resources (`@salesforce/resourceUrl/*`) & content assets
277
+
278
+ **No provider.** Off-platform there is no static resource CDN. Import assets through
279
+ Vite instead (they get emitted into `dist/assets/`, or inlined into `index.html` if you
280
+ build single-file), or reference them by absolute URL if hosted separately.
281
+
282
+ ### Org / user / page context wires
283
+
284
+ Wire adapters that inject ambient context — `$CurrentPageReference`, current user,
285
+ org info — have **no provider**. Pass the values your component needs in explicitly at
286
+ mount time (`bootstrap.js`) or read them from the URL/host, rather than wiring them.
287
+
288
+ ---
289
+
290
+ ## Data access and authentication
291
+
292
+ Even when your data code is correct, _where the bundle is served from_ determines
293
+ whether it can reach the org. Off-platform there are **two distinct data paths**, and
294
+ they resolve differently:
295
+
296
+ 1. **MCP-backed adapters** — `lightning/graphql` (`@wire(graphql)`) and the registered
297
+ `lightning/uiRecordApi` exports. These dispatch through the **host bridge** exposed
298
+ by `@salesforce/platform-sdk` — `getChatSDK().callTool(name, params)` with tool names
299
+ like `graphqlQuery`, `getRecordMcpTool`, …. The SDK detects the runtime surface and
300
+ picks the transport: on ChatGPT it calls `window.openai.callTool`; on MCP Apps it
301
+ sends a JSON-RPC `tools/call` request over the app's message channel. Either way the
302
+ adapter code is transport-agnostic. These do **not** hit `/services/*` and do **not**
303
+ use `lwcProxy`.
304
+ 2. **Same-origin REST** — the imperative Data SDK (`createDataSDK().graphql.query()` /
305
+ `.fetch()`) and any legacy `lightning/*` module that calls `/services/*` directly.
306
+ These make same-origin HTTP requests and need a real org session.
307
+
308
+ How each path gets live data, by surface:
309
+
310
+ | Surface | MCP-backed adapters | Same-origin REST (Data SDK / legacy) |
311
+ | ------------------------------------- | -------------------------------- | --------------------------------------------------- |
312
+ | **Local dev (`npm run dev`)** | mock the host bridge (see below) | `lwcProxy()` forwards `/services/*` to `sf` CLI org |
313
+ | **Real MCP host (ChatGPT, MCP Apps)** | host provides `callTool` | n/a on this surface |
314
+ | **Deployed to `*.salesforce.app`** | host bridge, if present | authenticated same-origin session |
315
+ | **Raw static file / other origin** | no host bridge → no data | no org session — `/services/data/*` returns `401` |
316
+
317
+ Key facts:
318
+
319
+ - **`lwcProxy()` is development-only and REST-only.** It runs inside the Vite dev
320
+ server (not in the production `dist/index.html`) and exists for the same-origin REST
321
+ path — the Data SDK's imperative calls and legacy `lightning/*` modules that hit
322
+ `/services/*`. The MCP-backed `lightning/graphql` and `lightning/uiRecordApi`
323
+ adapters bypass it entirely; drive those with a real host or, in dev, a mocked host
324
+ bridge. On the ChatGPT surface that mock is a guarded `window.openai.callTool` shim in
325
+ your entry script; other surfaces (MCP Apps) resolve the bridge through their own
326
+ transport, so the SDK's `getChatSDK().callTool` picks the right one automatically.
327
+ - **A deployed bundle needs the `*.salesforce.app` app domain** for the same-origin
328
+ REST path. Served from that domain, the Data SDK's cookie+CSRF flow against
329
+ `/services/data/v{version}/graphql` succeeds. Served from a different origin (a plain
330
+ static host, or the `my.salesforce.com` org domain), the same calls `401` — this is
331
+ by design, not a bug in your code.
332
+ - **The Data SDK methods are optional.** Both `graphql?.query(...)` and `fetch?.()` may
333
+ be unavailable depending on the runtime surface — always call them with optional
334
+ chaining, and handle the `undefined` case.
335
+
336
+ For the full data-access API, see [`@salesforce/platform-sdk`](../../sdk/platform-sdk/README.md).
337
+
338
+ ---
339
+
340
+ ## Quick reference: "which workaround?"
341
+
342
+ | You were using… | Off-platform replacement |
343
+ | --------------------------------------------- | ---------------------------------------------------------------- |
344
+ | Imperative `@AuraEnabled` Apex | GraphQL query, or `@RestResource` + `sdk.fetch?.()` |
345
+ | `getRecord` / `createRecord` / `updateRecord` | Works via `lds()` (registered) — no change |
346
+ | Other LDS adapter (`getListUi`, …) | Register an MCP tool via `lds({ … })`, or use GraphQL |
347
+ | `lightning/messageService` | `EventTarget` stub (same-bundle) or host bridge (cross-surface) |
348
+ | `lightning/navigation` | No-op `NavigationMixin` stub + explicit routing |
349
+ | `@salesforce/label/*` | `builtins.label()` (static) or `builtins.labelsGraphql()` (live) |
350
+ | `@salesforce/gate/*` | `builtins.gate()` with overrides (defaults open) |
351
+ | Static resource URL | Import the asset through Vite |
352
+ | Wire that auto-refreshes on store change | `refresh()` or manual re-query — no reactive store off-platform |
353
+
354
+ ---
355
+
356
+ ## Reference
357
+
358
+ - [Migration Guide](migration-guide.md) — applying these workarounds step by step
359
+ - [Consumer Guide](consumer-guide.md) — baseline setup and local-dev options
360
+ - [Plugin README → Built-in Providers](../README.md#built-in-providers) — provider API
361
+ - [Plugin README → `lds(adapters?)`](../README.md#ldsadapters) — registering adapters
362
+ - [`lwc-records` example](../../../examples/lwc-axl/lwc-records) — real stubs for
363
+ `force/navigation`, `aura`, `logger`
364
+ - [`@salesforce/platform-sdk`](../../sdk/platform-sdk/README.md) — GraphQL + REST data access
365
+ </content>
@@ -0,0 +1,334 @@
1
+ # Migrating Platform LWC to an LWC UI Bundle
2
+
3
+ This guide walks you through porting a Lightning Web Component that runs **inside a
4
+ Salesforce org** to an **LWC UI Bundle** compiled off-platform with
5
+ `@salesforce/vite-plugin-lwc-ui-bundle`. The build emits a static `dist/` (an
6
+ `index.html` plus hashed `assets/*`, or a single inlined `dist/index.html` if you add
7
+ `vite-plugin-singlefile`) that runs in any browser, an MCP host, or a Salesforce UI
8
+ Bundle served from the `*.salesforce.app` domain.
9
+
10
+ > **New to the plugin?** Read the [Consumer Guide](consumer-guide.md) first for the
11
+ > baseline Vite setup. This guide focuses on what _changes_ when the component was
12
+ > originally written for the platform, and links to the
13
+ > [Limitations reference](limitations.md) for the features that don't come across.
14
+
15
+ ---
16
+
17
+ ## What actually changes
18
+
19
+ A platform LWC and an LWC UI Bundle share the **same component source** — `.js`,
20
+ `.html`, and `.css` files compile unchanged through `@lwc/rollup-plugin`. What
21
+ changes is the _environment_ your component runs in:
22
+
23
+ | On platform | In an LWC UI Bundle |
24
+ | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
25
+ | The Lightning runtime mounts your component | You mount it yourself in `bootstrap.js` via `createElement` |
26
+ | `@salesforce/*` modules resolve to live org services | Scoped **providers** resolve them to generated JS (labels, i18n, gates, …) |
27
+ | LDS wire adapters read from a reactive client-side store | Registered adapters dispatch through the Data SDK's host bridge to MCP tools; no reactive store |
28
+ | Imperative Apex, LMS, and navigation are ambient | They have **no provider** — you supply a stub or a GraphQL/REST replacement |
29
+ | Metadata (`.js-meta.xml`) drives exposure and targets | `.js-meta.xml` is ignored by the build; a `ui-bundle` manifest drives routing |
30
+
31
+ The migration is therefore mostly about **the edges** of your component — how it gets
32
+ mounted, how it gets data, and which platform capabilities it reaches for. The
33
+ component logic in the middle usually ports verbatim.
34
+
35
+ ---
36
+
37
+ ## Migration at a glance
38
+
39
+ 1. [Inventory your dependencies](#step-1-inventory-your-dependencies) — find every
40
+ `import` that reaches the platform.
41
+ 2. [Set up the Vite project](#step-2-set-up-the-vite-project) — `vite.config.js`,
42
+ `index.html`, `bootstrap.js`.
43
+ 3. [Map each dependency](#step-3-map-each-dependency) to a provider, an SDK call, or
44
+ a stub.
45
+ 4. [Replace data access](#step-4-replace-data-access) — LDS/Apex → GraphQL or the
46
+ Data SDK.
47
+ 5. [Handle the unsupported edges](#step-5-handle-the-unsupported-edges) — LMS,
48
+ navigation, and other org-only APIs.
49
+ 6. [Build, run, and verify](#step-6-build-run-and-verify).
50
+
51
+ ---
52
+
53
+ ## Step 1: Inventory your dependencies
54
+
55
+ Before touching config, list every non-relative import in your component tree. These
56
+ are the imports that reach beyond your own `.js`/`.html` files and into the platform:
57
+
58
+ ```bash
59
+ # From your LWC source root — list the platform-facing specifiers your bundle uses
60
+ grep -rhoE "from \"(@salesforce/[^\"]+|lightning/[^\"]+|force/[^\"]+|aura)\"" \
61
+ force-app/main/default/lwc | sort -u
62
+ ```
63
+
64
+ Sort what you find into four buckets — this table _is_ your migration plan:
65
+
66
+ | Import pattern | Bucket | Action |
67
+ | ----------------------------------------------------------------------------------------------------- | ------------------ | ------------------------------------------------------------------------------------ |
68
+ | `lwc`, `lightning/*` base components | **Compiles as-is** | Nothing — bundled via `@lwc/rollup-plugin` + npm |
69
+ | `@salesforce/label/*`, `@salesforce/i18n/*`, `client`, `gate`, `accessCheck` | **Has a provider** | Add the matching `builtins.*` provider |
70
+ | `lightning/graphql`, registered LDS adapters | **Data access** | See [Step 4](#step-4-replace-data-access) |
71
+ | `@salesforce/apex/*`, `lightning/messageService`, `lightning/navigation`, `force/*`, `aura`, `logger` | **Unsupported** | Replace with GraphQL/REST or a stub — [Step 5](#step-5-handle-the-unsupported-edges) |
72
+
73
+ Anything in the last two buckets needs a decision. The [Limitations
74
+ reference](limitations.md) covers each unsupported module and its recommended
75
+ workaround in detail.
76
+
77
+ ---
78
+
79
+ ## Step 2: Set up the Vite project
80
+
81
+ Follow [Consumer Guide → Off-Platform Build](consumer-guide.md#off-platform-build) to
82
+ add `vite.config.js`, `index.html`, and `bootstrap.js`. The one part worth calling out
83
+ for migrations is that **you now own mounting**. On platform, the framework
84
+ instantiated your top-level component; off-platform, `bootstrap.js` does:
85
+
86
+ ```js
87
+ // bootstrap.js
88
+ import "@salesforce-ux/design-system/assets/styles/salesforce-lightning-design-system.css";
89
+ import "@lwc/synthetic-shadow";
90
+ import { createElement } from "lwc";
91
+ import App from "c/myApp"; // your existing root component, unchanged
92
+
93
+ const el = createElement("c-my-app", { is: App });
94
+
95
+ // Public @api properties the org used to set — set them here instead
96
+ el.recordId = "001xx000003DGb2AAG";
97
+
98
+ document.getElementById("app").appendChild(el);
99
+ ```
100
+
101
+ Any `@api` property the org used to inject (`recordId`, `objectApiName`, flexipage
102
+ attributes, etc.) now has no injector — set it explicitly when you mount, or read it
103
+ from the URL/host. This is the single most common migration surprise.
104
+
105
+ ---
106
+
107
+ ## Step 3: Map each dependency
108
+
109
+ For every import in the **"has a provider"** bucket, add the matching provider to
110
+ your `vite.config.js`. The defaults cover the common set:
111
+
112
+ ```js
113
+ import lwcVitePlugin, { builtins } from "@salesforce/vite-plugin-lwc-ui-bundle";
114
+
115
+ lwcVitePlugin({
116
+ modules: {
117
+ dirs: [{ path: "force-app/main/default/lwc", namespace: "c" }],
118
+ npm: ["lightning-base-components"],
119
+ },
120
+ providers: [
121
+ builtins.label(), // @salesforce/label/*
122
+ builtins.i18n(), // @salesforce/i18n/*
123
+ builtins.accessCheck(), // @salesforce/accessCheck/* (default false)
124
+ builtins.client(), // @salesforce/client/*
125
+ builtins.gate(), // @salesforce/gate/* (default open)
126
+ builtins.primitiveUtils(), // lightning/primitiveUtils
127
+ builtins.lds(), // lightning/uiRecordApi, lightning/graphql, … (MCP-backed)
128
+ ],
129
+ });
130
+ ```
131
+
132
+ If you pass a `providers` array, it **replaces** the defaults — list every provider
133
+ you need. Omit `providers` entirely to get the full default set: `label`, `i18n`,
134
+ `accessCheck`, `client`, `gate`, `primitiveUtils`, and `lds`. Note there is no separate
135
+ GraphQL provider — `lightning/graphql` is handled by `lds()`, whose default registry
136
+ routes `graphql`/`executeMutation` to an MCP tool.
137
+
138
+ A few provider-specific notes for migrated code:
139
+
140
+ - **Labels** default to a human-readable fallback derived from the key
141
+ (`c.appTitle` → "App Title"). Port real values with
142
+ `builtins.label({ "c.appTitle": "My App" })`, or copy them from your
143
+ `CustomLabels.labels-meta.xml`. If you have a live data path and want translated
144
+ values at the user's locale, swap in `builtins.labelsGraphql()` instead — see
145
+ [Limitations → Custom Labels](limitations.md#custom-labels--static-default-or-live-via-graphql).
146
+ - **Gates** default **open** and **access checks** default **false** off-platform.
147
+ If your component branches on either, pass overrides so its behavior matches the
148
+ org: `builtins.gate({ myGate: false })`, `builtins.accessCheck({ MyPerm: true })`.
149
+ - **i18n** derives locale/currency from the browser via the `Intl` API; CLDR format
150
+ patterns use en-US defaults. Locale-identity is live, format specifics are static.
151
+
152
+ ---
153
+
154
+ ## Step 4: Replace data access
155
+
156
+ This is the heart of most migrations. Platform LWC reaches org data three ways:
157
+ **LDS wire adapters**, **imperative Apex**, and **`lightning/graphql`**. Off-platform,
158
+ GraphQL is the through-line — it works in both a component wire and imperatively via
159
+ the Data SDK.
160
+
161
+ ### LDS wire adapters (`getRecord`, `createRecord`, …)
162
+
163
+ The `lds()` provider (on by default) routes a **registered** set of adapters to MCP
164
+ tools. Out of the box that covers `getRecord`, `createRecord`, `updateRecord`
165
+ (`lightning/uiRecordApi`) and `getObjectInfo_imperative` (`lightning/uiObjectInfoApi`). Code
166
+ that uses only these ports **unchanged**:
167
+
168
+ ```js
169
+ import { getRecord } from "lightning/uiRecordApi";
170
+
171
+ // Same @wire on platform and off — the adapter resolves to an MCP tool off-platform
172
+ @wire(getRecord, { recordId: "$recordId", fields: FIELDS })
173
+ wiredRecord({ data, error }) { /* ... */ }
174
+ ```
175
+
176
+ Two behavioral differences to plan for:
177
+
178
+ - **No reactive store.** `subscribe()` on the imperative read shapes is a deliberate
179
+ no-op — the callback never fires. Data does not auto-refresh when it changes
180
+ elsewhere; call `refresh()` (on the `subscribable-refreshable` shape) or re-query.
181
+ - **Only registered exports resolve.** Any LDS export not in the registry (e.g.
182
+ `getRelatedListRecords`, `getListUi`) passes through to normal `lightning/*`
183
+ resolution — which has no off-platform implementation, so the build fails to resolve
184
+ it. Register it with your own MCP tool via the `lds({ ... })` config, or replace the
185
+ call with GraphQL. See [Limitations → LDS](limitations.md#lds--partial-adapter-coverage-no-store).
186
+
187
+ ### Imperative Apex (`@salesforce/apex/MyClass.myMethod`)
188
+
189
+ There is **no Apex provider**, and `@AuraEnabled` methods are **not** reachable
190
+ off-platform. Migrate each imperative Apex call to one of:
191
+
192
+ 1. **GraphQL** — if the method just reads records, replace it with a UI API GraphQL
193
+ query (preferred; see below).
194
+ 2. **Apex REST** — expose the logic as an `@RestResource` and call it through the Data
195
+ SDK's `fetch`. Budget time to build this surface for any business logic you can't
196
+ express as GraphQL.
197
+
198
+ See [Limitations → Imperative Apex](limitations.md#imperative-apex-salesforceapexmyclassmymethod)
199
+ for the `sdk.fetch?.()` snippet and the `@AuraEnabled`-vs-`@RestResource` distinction.
200
+
201
+ ### `lightning/graphql` and the Data SDK
202
+
203
+ `lightning/graphql` keeps working via the default `builtins.lds()` registry — no
204
+ separate provider, and `@wire(graphql, …)` components port unchanged. The wire adapter
205
+ dispatches through the MCP `graphqlQuery` tool. For imperative reads, use
206
+ `@salesforce/platform-sdk` directly:
207
+
208
+ ```js
209
+ import { createDataSDK, gql } from "@salesforce/platform-sdk";
210
+
211
+ const sdk = await createDataSDK();
212
+ const result = await sdk.graphql?.query({
213
+ query: gql`
214
+ query GetAccounts {
215
+ uiapi {
216
+ query {
217
+ Account(first: 10) {
218
+ edges {
219
+ node {
220
+ Id
221
+ Name {
222
+ value
223
+ }
224
+ }
225
+ }
226
+ }
227
+ }
228
+ }
229
+ }
230
+ `,
231
+ });
232
+ ```
233
+
234
+ `sdk.graphql?.query(...)` resolves against whatever surface the bundle runs on — a real
235
+ MCP host's bridge, or the authenticated same-origin session on `*.salesforce.app`. Where
236
+ each data path gets live data (and why a bundle on any other origin `401`s) is covered in
237
+ [Limitations → Data access & auth](limitations.md#data-access-and-authentication).
238
+
239
+ For **local development without a real host**, install a guarded host-bridge mock in
240
+ your entry script (on ChatGPT that's a `window.openai.callTool` shim) so
241
+ `@wire(graphql)` / `@wire(getRecord)` return data — see
242
+ [Consumer Guide → Step 4: `bootstrap.js`](consumer-guide.md#step-4-create-bootstrapjs).
243
+ (`lwcProxy()` is a separate, optional companion plugin — it forwards `/services/*` for
244
+ _legacy_ `lightning/*` modules that call REST directly, not for the MCP-backed LDS and
245
+ GraphQL adapters.)
246
+
247
+ ---
248
+
249
+ ## Step 5: Handle the unsupported edges
250
+
251
+ Some platform capabilities have no off-platform equivalent. The repo's convention is
252
+ a **hand-written stub** wired through the plugin's `stubs` option — the same pattern
253
+ the [`lwc-records` example](../../../examples/lwc-axl/lwc-records) uses for
254
+ `force/navigation`, `aura`, and `logger`.
255
+
256
+ Each unsupported capability has a recommended stub or replacement documented in the
257
+ Limitations reference — this step is about wiring them through the plugin's `stubs`
258
+ option. The pattern is the same for each: point the specifier at a hand-written stub
259
+ file that exports the shapes your code imports.
260
+
261
+ ```js
262
+ // vite.config.js — one entry per unsupported specifier
263
+ lwcVitePlugin({
264
+ stubs: {
265
+ "lightning/navigation": "src/stubs/navigation.js",
266
+ "lightning/messageService": "src/stubs/message-service.js",
267
+ aura: "src/stubs/aura.js",
268
+ "force/someModule": "src/stubs/some-module.js",
269
+ },
270
+ });
271
+ ```
272
+
273
+ For the actual stub contents and per-module guidance:
274
+
275
+ - **Navigation** (`lightning/navigation`, `force/navigation`) — no-op `NavigationMixin`,
276
+ then decide per call site: [Limitations → Navigation](limitations.md#navigation-lightningnavigation-forcenavigation).
277
+ - **Lightning Message Service** (`lightning/messageService`) — `EventTarget`-backed bus
278
+ for same-bundle messaging, host bridge across surfaces:
279
+ [Limitations → LMS](limitations.md#lightning-message-service-lightningmessageservice).
280
+ - **`aura`, `logger`, `force/*` and other core-only modules** — export just the bindings
281
+ your code imports (often no-ops): [Limitations → Aura and other `force/*`](limitations.md#aura-and-other-force--core-only-modules).
282
+
283
+ If the build fails with `Rollup failed to resolve import "force/someModule"` (or
284
+ `aura`), that specifier needs a stub. The `lwc-records` example ships real stubs for
285
+ `force/navigation`, `aura`, and `logger`.
286
+
287
+ ---
288
+
289
+ ## Step 6: Build, run, and verify
290
+
291
+ ```bash
292
+ npm run build # → dist/ (index.html + assets/*, or a single dist/index.html
293
+ # if you added vite-plugin-singlefile)
294
+ open dist/index.html
295
+
296
+ npm run dev # dev server with live reload (+ lwcProxy for live data)
297
+ ```
298
+
299
+ A migration is done when:
300
+
301
+ - The bundle **builds** with no unresolved imports (unresolved = a dependency you
302
+ haven't yet mapped to a provider, SDK call, or stub — go back to Step 3).
303
+ - Every `@api` the org used to set is set at mount time or read from the host/URL.
304
+ - Data reads succeed against your target surface (mock, `lwcProxy` dev, or a
305
+ `*.salesforce.app`-served deployed bundle).
306
+ - Behavior that branched on gates/access checks matches the org, given your provider
307
+ overrides.
308
+
309
+ ---
310
+
311
+ ## Common migration errors
312
+
313
+ | Symptom | Cause | Fix |
314
+ | ------------------------------------------------------------ | -------------------------------------------------- | ----------------------------------------------------------------------- |
315
+ | `Rollup failed to resolve import "@salesforce/apex/…"` | No Apex provider | Migrate to GraphQL or Apex REST ([Step 4](#step-4-replace-data-access)) |
316
+ | `Rollup failed to resolve import "lightning/navigation"` | No navigation provider | Add a stub ([Step 5](#step-5-handle-the-unsupported-edges)) |
317
+ | `Rollup failed to resolve import "force/…"` | Core-only module | Add a stub via `stubs` |
318
+ | `@wire(getRelatedListRecords)` builds but returns nothing | Adapter not in the `lds()` registry | Register an MCP tool or replace with GraphQL |
319
+ | Wire data never refreshes | `subscribe()` is a no-op off-platform | Call `refresh()` or re-query explicitly |
320
+ | `Cannot read properties of undefined (reading 'isOpen')` | Missing `gate()` provider (base components use it) | Add `builtins.gate()` |
321
+ | Component mounts but is missing data it used to get for free | `@api` no longer injected by the platform | Set the property in `bootstrap.js` |
322
+ | Deployed bundle `401`s on `/services/data/*` | Served from a non-`salesforce.app` origin | Serve from `*.salesforce.app`, or use `lwcProxy` in dev |
323
+
324
+ ---
325
+
326
+ ## Reference
327
+
328
+ - [Consumer Guide](consumer-guide.md) — baseline Vite setup and local-dev options
329
+ - [Limitations & Unsupported Features](limitations.md) — the full constraint reference
330
+ - [`lwc-records` example](../../../examples/lwc-axl/lwc-records) — real stubs for
331
+ `force/navigation`, `aura`, `logger`
332
+ - [`@salesforce/platform-sdk`](../../sdk/platform-sdk/README.md) — GraphQL + REST data access
333
+ </content>
334
+ </invoke>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/vite-plugin-lwc-ui-bundle",
3
- "version": "11.13.0",
3
+ "version": "11.13.2",
4
4
  "description": "Vite plugin for compiling LWC components into static bundles for off-platform and MCP use",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "author": "Salesforce",
@@ -74,9 +74,9 @@
74
74
  },
75
75
  "peerDependencies": {
76
76
  "@lwc/rollup-plugin": "^9.0.0",
77
- "@salesforce/platform-sdk": "^11.13.0",
77
+ "@salesforce/platform-sdk": "^11.13.2",
78
78
  "@salesforce/state-managers-uiapi": "^0.31.0",
79
- "@salesforce/ui-bundle": "^11.13.0",
79
+ "@salesforce/ui-bundle": "^11.13.2",
80
80
  "lwc": "^9.0.0",
81
81
  "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0",
82
82
  "zod": "^3.23.8"
@@ -100,7 +100,7 @@
100
100
  "devDependencies": {
101
101
  "@conduit-client/bindings-utils": "3.19.6",
102
102
  "@conduit-client/command-base": "3.19.6",
103
- "@salesforce/platform-sdk": "^11.13.0",
103
+ "@salesforce/platform-sdk": "^11.13.2",
104
104
  "@types/ws": "^8.5.12",
105
105
  "typescript": "^5.9.3",
106
106
  "vite": "^7.0.0",