@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
@@ -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.1",
3
+ "version": "11.14.0",
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",
@@ -49,7 +49,7 @@
49
49
  "skills"
50
50
  ],
51
51
  "scripts": {
52
- "build": "vite build && vite build --mode runtime-lds && vite build --mode runtime-labels-graphql",
52
+ "build": "vite build && vite build --mode runtime-lds && vite build --mode runtime-labels-graphql && vite build --mode runtime-platform-graphql",
53
53
  "clean": "rm -rf dist",
54
54
  "dev": "vite build --watch",
55
55
  "test": "vitest run",
@@ -74,9 +74,9 @@
74
74
  },
75
75
  "peerDependencies": {
76
76
  "@lwc/rollup-plugin": "^9.0.0",
77
- "@salesforce/platform-sdk": "^11.13.1",
77
+ "@salesforce/platform-sdk": "^11.14.0",
78
78
  "@salesforce/state-managers-uiapi": "^0.31.0",
79
- "@salesforce/ui-bundle": "^11.13.1",
79
+ "@salesforce/ui-bundle": "^11.14.0",
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.1",
103
+ "@salesforce/platform-sdk": "^11.14.0",
104
104
  "@types/ws": "^8.5.12",
105
105
  "typescript": "^5.9.3",
106
106
  "vite": "^7.0.0",
@@ -157,7 +157,15 @@ Starting from the root component, trace the dependency tree:
157
157
  providers because base components use these modules internally even
158
158
  if user code doesn't
159
159
  - `@salesforce/gate/*`, `@salesforce/accessCheck/*` → handled by
160
- those providers
160
+ those providers (accessCheck deny-defaults to `false`, no per-name
161
+ config required)
162
+ - `@salesforce/userPermission/<Name>`, `@salesforce/customPermission/<Name>`
163
+ → **collect every `<Name>`.** These are named per-user permissions:
164
+ the default `platformGraphql()` provider **fails the build** unless
165
+ each imported name has a first-paint value declared in
166
+ `userPermissionDefaults` / `customPermissionDefaults`. Record the
167
+ exact names so Step 7 can generate them (default them to `false`
168
+ unless the user says otherwise). See `known-pitfalls.md#17`.
161
169
  - `@salesforce/i18n/*` → `i18n` provider
162
170
  - `@salesforce/client/*` → `client` provider (provides `formFactor`
163
171
  based on viewport width)
@@ -227,6 +235,13 @@ Check in this order:
227
235
  present (the default registry covers all three specifiers)
228
236
  - Step 4 found any `@salesforce/i18n/*` → `builtins.i18n()` must be
229
237
  present
238
+ - Step 4 found any `@salesforce/userPermission/<Name>` or
239
+ `@salesforce/customPermission/<Name>` → the default
240
+ `platformGraphql()` provider must declare a first-paint value for
241
+ **each** name in `userPermissionDefaults` / `customPermissionDefaults`,
242
+ or the build throws. Use the `defaultProviders({ platformGraphql: {…} })`
243
+ helper so the rest of the registry stays default. See
244
+ `references/known-pitfalls.md#17`.
230
245
  - Step 4 found any `@salesforce/client/*` → `builtins.client()` must
231
246
  be present
232
247
  - `modules.npm` includes `lightning-base-components` (or
@@ -465,6 +480,34 @@ Adapt based on earlier findings:
465
480
 
466
481
  - Set `dirs` for the detected project structure (SFDX vs namespaced).
467
482
  - Populate `builtins.label({...})` with values from Step 6.
483
+ - **Declare a first-paint value for every named permission Step 4 found.**
484
+ Each `@salesforce/userPermission/<Name>` / `@salesforce/customPermission/<Name>`
485
+ import needs an entry, or the build throws (`known-pitfalls.md#17`).
486
+ Prefer the `defaultProviders()` helper so you configure only this while
487
+ keeping the rest of the default registry:
488
+
489
+ ```js
490
+ import lwcVitePlugin, { defaultProviders } from "@salesforce/vite-plugin-lwc-ui-bundle";
491
+
492
+ lwcVitePlugin({
493
+ modules: {
494
+ /* ... */
495
+ },
496
+ providers: defaultProviders({
497
+ platformGraphql: {
498
+ userPermissionDefaults: { ApiEnabled: false, CustomizeApplication: false },
499
+ customPermissionDefaults: { My_Custom_Perm: false },
500
+ },
501
+ }),
502
+ });
503
+ ```
504
+
505
+ Default each to `false` unless the user wants a `true` first paint; note
506
+ the resolved org value only reaches the DOM if the component imports the
507
+ module's `subscribe()` export (keep a plain default import if the same
508
+ source also deploys on-platform). `@salesforce/accessCheck/*` needs no
509
+ such config — it deny-defaults to `false`.
510
+
468
511
  - Include `builtins.primitiveUtils()` if using `lightning-base-components`.
469
512
  - Include `builtins.lds()` if any component uses `lightning/uiRecordApi`,
470
513
  `lightning/uiObjectInfoApi`, `lightning/graphql`, or any of the
@@ -440,3 +440,60 @@ the plugin with `livePreview({ debug: true })` (or run with plugin debug
440
440
  logging) — it logs the underlying import error. Note the bridge is a
441
441
  local-dev convenience only; its absence never affects the compiled
442
442
  bundle.
443
+
444
+ ## 17. Build fails: named permission imported with no first-paint value
445
+
446
+ **Symptom:** `vite build` (or `npm run dev`) fails with:
447
+
448
+ ```
449
+ [platform-graphql] @salesforce/userPermission/ApiEnabled was imported but
450
+ has no first-paint value configured. A permission is per-user runtime
451
+ state, so the plugin refuses to silently render a fabricated `false`. …
452
+ ```
453
+
454
+ **Cause:** A component imports `@salesforce/userPermission/<Name>` or
455
+ `@salesforce/customPermission/<Name>`, but no value was declared for that
456
+ name. A named permission is an assertion about **per-user runtime state**;
457
+ a plain default import binds the value **once** at module-eval time and
458
+ never updates on its own (only the module's `subscribe()` export does).
459
+ So a silently-defaulted `false` would render a permanent, wrong answer —
460
+ even when the GraphQL fetch **succeeds**. Rather than guess, the plugin
461
+ requires you to state the value shown before the org responds. (This is
462
+ **not** the same as `@salesforce/accessCheck/*`, which is a feature gate
463
+ and safely deny-defaults to `false` — see pitfall #8.)
464
+
465
+ **Fix:** Declare each imported permission's first-paint value via the
466
+ `defaultProviders()` helper (keeps every other default in place):
467
+
468
+ ```js
469
+ import lwcVitePlugin, { defaultProviders } from "@salesforce/vite-plugin-lwc-ui-bundle";
470
+
471
+ lwcVitePlugin({
472
+ modules: {
473
+ /* ... */
474
+ },
475
+ providers: defaultProviders({
476
+ platformGraphql: {
477
+ userPermissionDefaults: { ApiEnabled: false, CustomizeApplication: false },
478
+ customPermissionDefaults: { My_Custom_Perm: false },
479
+ },
480
+ }),
481
+ });
482
+ ```
483
+
484
+ Use `false` unless you have a specific reason to render `true` before the
485
+ fetch resolves. To reflect the **resolved** org value in the DOM, import
486
+ the module's `subscribe(callback)` export and assign to a reactive field:
487
+
488
+ ```js
489
+ import apiEnabled, { subscribe } from "@salesforce/userPermission/ApiEnabled";
490
+ // this.apiEnabled = apiEnabled; // first paint (configured)
491
+ // connectedCallback() { subscribe(v => this.apiEnabled = v); } // corrects in DOM
492
+ ```
493
+
494
+ If the same component must also deploy on-platform (where the scoped
495
+ module has no `subscribe` export), keep the plain default import.
496
+
497
+ **Prevention:** SKILL.md Step 4 flags every `@salesforce/userPermission/*`
498
+ and `@salesforce/customPermission/*` import so the generated config
499
+ declares its first-paint value up front.