@salesforce/vite-plugin-lwc-ui-bundle 11.13.1 → 11.14.0

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.
Files changed (38) hide show
  1. package/README.md +3 -0
  2. package/dist/index.d.ts +32 -1
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +30 -5
  5. package/dist/index.js.map +1 -1
  6. package/dist/providers/access-check.d.ts.map +1 -1
  7. package/dist/providers/gate.d.ts.map +1 -1
  8. package/dist/providers/i18n.d.ts +16 -0
  9. package/dist/providers/i18n.d.ts.map +1 -1
  10. package/dist/providers/index.d.ts +2 -0
  11. package/dist/providers/index.d.ts.map +1 -1
  12. package/dist/providers/index.js +3 -178
  13. package/dist/providers/index.js.map +1 -1
  14. package/dist/providers/labels-graphql/index.d.ts.map +1 -1
  15. package/dist/providers/labels-graphql/index.js +12 -3
  16. package/dist/providers/labels-graphql/index.js.map +1 -1
  17. package/dist/providers/labels-graphql/runtime.d.ts +21 -14
  18. package/dist/providers/labels-graphql/runtime.d.ts.map +1 -1
  19. package/dist/providers/labels-graphql/runtime.js +39 -26
  20. package/dist/providers/labels-graphql/runtime.js.map +1 -1
  21. package/dist/providers/platform-graphql/constants.d.ts +15 -0
  22. package/dist/providers/platform-graphql/constants.d.ts.map +1 -0
  23. package/dist/providers/platform-graphql/i18n-static.d.ts +19 -0
  24. package/dist/providers/platform-graphql/i18n-static.d.ts.map +1 -0
  25. package/dist/providers/platform-graphql/index.d.ts +58 -0
  26. package/dist/providers/platform-graphql/index.d.ts.map +1 -0
  27. package/dist/providers/platform-graphql/index.js +312 -0
  28. package/dist/providers/platform-graphql/index.js.map +1 -0
  29. package/dist/providers/platform-graphql/runtime.d.ts +30 -0
  30. package/dist/providers/platform-graphql/runtime.d.ts.map +1 -0
  31. package/dist/providers/platform-graphql/runtime.js +189 -0
  32. package/dist/providers/platform-graphql/runtime.js.map +1 -0
  33. package/docs/consumer-guide.md +125 -4
  34. package/docs/limitations.md +365 -0
  35. package/docs/migration-guide.md +334 -0
  36. package/package.json +5 -5
  37. package/skills/setup-lwc-vite-plugin/SKILL.md +44 -1
  38. package/skills/setup-lwc-vite-plugin/references/known-pitfalls.md +57 -0
@@ -259,6 +259,21 @@ export default defineConfig({
259
259
  });
260
260
  ```
261
261
 
262
+ **What is the `providers` array?** It is the list of resolvers for `@salesforce/*`
263
+ scoped-module imports (`@salesforce/label/*`, `@salesforce/i18n/*`,
264
+ `@salesforce/userPermission/*`, `@salesforce/gate/*`, …) that don't exist as
265
+ real npm packages. Each `builtins.*()` returns a Vite plugin that intercepts one
266
+ family of specifiers and generates the module the platform LWC compiler would
267
+ otherwise provide.
268
+
269
+ **You usually don't need it.** When you **omit** `providers` entirely, the plugin
270
+ installs a default set that resolves labels, i18n, permissions, and access checks
271
+ at runtime via GraphQL (see [GraphQL-backed scoped modules](#graphql-backed-scoped-modules-default)
272
+ below). Pass `providers` only to opt a family **out** of the runtime fetch (e.g.
273
+ `builtins.i18n()` for browser/`Intl`-derived locale identity, `builtins.label()`
274
+ for static build-time labels) or to add extra config — the explicit list above
275
+ shows the opt-out shape, not a required one.
276
+
262
277
  #### Component Directory Configuration
263
278
 
264
279
  The plugin supports two directory structures:
@@ -298,8 +313,22 @@ Components are importable as `myNamespace/myComponent`.
298
313
 
299
314
  #### Configuring Labels
300
315
 
301
- The `builtins.label()` provider handles `@salesforce/label/*` imports. How you
302
- configure it depends on your project:
316
+ There are two providers for `@salesforce/label/*`:
317
+
318
+ - **`builtins.labelsGraphql()` — the default.** Resolves labels at runtime via UI
319
+ API GraphQL through the Platform Data SDK (`createDataSDK()`), so labels reflect
320
+ the current user's translation. The SDK picks the transport per surface: a
321
+ direct session-authenticated GraphQL request in a full-page web app, or the
322
+ `window.openai` bridge in an MCP/ChatGPT host. Build-time values (or the
323
+ key-derived fallback) render until the runtime fetch resolves. Included
324
+ automatically when you omit the `providers` array.
325
+ - **`builtins.label()` — static.** Resolves to a fixed build-time string. Use it
326
+ to opt out of runtime fetching (e.g. a pure off-core demo with no org).
327
+
328
+ Both accept the same overrides object; `labelsGraphql` uses the overrides as the
329
+ static fallback shown before/instead of a successful GraphQL fetch.
330
+
331
+ How you configure labels depends on your project:
303
332
 
304
333
  **SFDX project with `CustomLabels.labels-meta.xml`:**
305
334
 
@@ -340,8 +369,95 @@ you'll get runtime errors like:
340
369
  Uncaught TypeError: Cannot read properties of undefined (reading 'isOpen')
341
370
  ```
342
371
 
343
- Always include `builtins.gate()` and `builtins.accessCheck()` when using
344
- `lightning-base-components`.
372
+ Include `builtins.gate()` when using `lightning-base-components`. (When you omit
373
+ the `providers` array entirely, the default set already covers these.)
374
+
375
+ #### GraphQL-backed scoped modules (default)
376
+
377
+ When you **omit** the `providers` array, these scoped modules resolve at runtime
378
+ via UI API GraphQL through the Platform Data SDK (with build-time first-paint
379
+ values):
380
+
381
+ | Scoped module | GraphQL source (`uiapi.platform.*`) | Provider | First-paint value |
382
+ | -------------------------------------- | ----------------------------------- | ------------------- | ------------------------------------ |
383
+ | `@salesforce/label/*` | `labels` | `labelsGraphql()` | override or key-derived text |
384
+ | `@salesforce/i18n/*` (locale identity) | `i18n` | `platformGraphql()` | CLDR placeholder |
385
+ | `@salesforce/userPermission/*` | `userPermissions` | `platformGraphql()` | **required — you must configure it** |
386
+ | `@salesforce/accessCheck/*` | `userPermissions` | `platformGraphql()` | `false` (deny-by-default) |
387
+ | `@salesforce/customPermission/*` | `customPermissions` | `platformGraphql()` | **required — you must configure it** |
388
+
389
+ The generated modules export the first-paint value as `default` and an opt-in
390
+ `subscribe(callback)` that fires with the resolved org value.
391
+
392
+ ##### Named permissions must be configured (no silent guess)
393
+
394
+ A `@salesforce/userPermission/<Name>` or `@salesforce/customPermission/<Name>`
395
+ import is an assertion about **per-user runtime state**. A plain default import
396
+ binds the module's value **once** at module-eval time and never updates on its
397
+ own — so if the plugin silently defaulted an unconfigured permission to `false`,
398
+ your UI would render a permanent, wrong answer about what the current user can do
399
+ (and it would be wrong even on the GraphQL **success** path, because a default
400
+ import never re-reads the resolved value).
401
+
402
+ Rather than guess, **the build fails** if you import a named permission without
403
+ declaring the value it should show before the org responds:
404
+
405
+ ```
406
+ [platform-graphql] @salesforce/userPermission/ApiEnabled was imported but has no
407
+ first-paint value configured. …
408
+ ```
409
+
410
+ Declare it with the `defaultProviders()` helper — which lets you configure this
411
+ one provider while keeping every other default in place:
412
+
413
+ ```js
414
+ import lwcVitePlugin, { defaultProviders } from "@salesforce/vite-plugin-lwc-ui-bundle";
415
+
416
+ lwcVitePlugin({
417
+ modules: { dirs: [{ path: "force-app/main/default/lwc", namespace: "c" }] },
418
+ providers: defaultProviders({
419
+ platformGraphql: {
420
+ userPermissionDefaults: { ApiEnabled: false, CustomizeApplication: false },
421
+ customPermissionDefaults: { My_Custom_Perm: false },
422
+ },
423
+ }),
424
+ });
425
+ ```
426
+
427
+ Use `false` unless you have a specific reason to render `true` before the fetch
428
+ resolves. `@salesforce/accessCheck/*` is exempt: it's a feature **gate**, so an
429
+ unconfigured check safely deny-defaults to `false` (base components rely on this),
430
+ though you can still override it.
431
+
432
+ ##### Reflecting the resolved value reactively (`subscribe`)
433
+
434
+ The configured value is only the **first paint**. To show the real org value once
435
+ the GraphQL fetch lands, import the module's `subscribe(callback)` export and
436
+ assign to a reactive field — the callback fires immediately with the current value
437
+ and again when the resolved value differs:
438
+
439
+ ```js
440
+ import apiEnabled, { subscribe } from "@salesforce/userPermission/ApiEnabled";
441
+
442
+ export default class extends LightningElement {
443
+ apiEnabled = apiEnabled; // first paint (your configured value)
444
+ connectedCallback() {
445
+ subscribe((v) => (this.apiEnabled = v)); // corrects in the DOM when the org responds
446
+ }
447
+ }
448
+ ```
449
+
450
+ > **Dual-deploy note:** the `subscribe` export exists only when the module is
451
+ > served by this plugin. If the **same** component source must also deploy as
452
+ > on-platform LWC metadata (where `@salesforce/userPermission/*` has no `subscribe`
453
+ > export), keep a plain default import — it will render the configured first-paint
454
+ > value off-core and the platform-resolved value on-core.
455
+
456
+ i18n number/date **format** patterns and calendar data stay static (CLDR reference
457
+ data, no org source). `@salesforce/gate/*` also stays static — its GraphQL field is
458
+ UiTier-context-only and not reachable off-core. The static `builtins.i18n()` /
459
+ `builtins.accessCheck()` / `builtins.label()` providers remain available for
460
+ explicit opt-out.
345
461
 
346
462
  ### Step 3: Create `index.html`
347
463
 
@@ -515,6 +631,9 @@ export const someExport = () => {};
515
631
  lwcVitePlugin({ stubs: { "force/someModule": "./stubs/someModule.js" } });
516
632
  ```
517
633
 
634
+ See [Limitations → Not supported](limitations.md#not-supported--and-how-to-work-around-it)
635
+ for stubbing navigation, LMS, Apex, and other core-only modules.
636
+
518
637
  ### Component renders but looks unstyled
519
638
 
520
639
  Add SLDS import to `bootstrap.js`:
@@ -527,6 +646,8 @@ import "@salesforce-ux/design-system/assets/styles/salesforce-lightning-design-s
527
646
 
528
647
  ## Reference
529
648
 
649
+ - **Migration Guide:** [migration-guide.md](migration-guide.md) — porting a platform LWC to a bundle
650
+ - **Limitations & Unsupported Features:** [limitations.md](limitations.md) — what's not supported off-platform, and the workaround for each gap
530
651
  - **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
652
  - **npm:** https://www.npmjs.com/package/@salesforce/vite-plugin-lwc-ui-bundle
532
653
  - **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>