@salesforce/vite-plugin-lwc-ui-bundle 1.133.0 → 1.133.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/dist/providers/lightning-graphql/runtime.js +3 -3
- package/dist/providers/lightning-graphql/runtime.js.map +1 -1
- package/package.json +2 -2
- package/skills/setup-lwc-vite-plugin/SKILL.md +337 -140
- package/skills/setup-lwc-vite-plugin/references/bootstrap-js-patterns.md +215 -0
- package/skills/setup-lwc-vite-plugin/references/chat-wrapper-flow.md +227 -0
- package/skills/setup-lwc-vite-plugin/references/known-pitfalls.md +215 -0
|
@@ -31,9 +31,9 @@ function gql(strings, ...values) {
|
|
|
31
31
|
async function runQuery(config) {
|
|
32
32
|
const { query, variables } = config;
|
|
33
33
|
if (!query) return { data: void 0, errors: void 0 };
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const result = await
|
|
34
|
+
const sdkRef = globalThis.__sfdc_sdk__;
|
|
35
|
+
if (sdkRef && typeof sdkRef.graphql === "function") {
|
|
36
|
+
const result = await sdkRef.graphql({ query, variables: variables ?? {} });
|
|
37
37
|
return { data: result?.data, errors: result?.errors };
|
|
38
38
|
}
|
|
39
39
|
const sdk = await getChatSDK();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime.js","sources":["../../../src/providers/shared/normalize-mcp-response.ts","../../../src/providers/lightning-graphql/runtime.ts"],"sourcesContent":["/**\n * Copyright (c) 2026, Salesforce, Inc.,\n * All rights reserved.\n * For full license text, see the LICENSE.txt file\n */\n\n/**\n * Unwraps the MCP tool transport envelope and returns the tool's payload as-is.\n *\n * Handles the three surface shapes `sdk.callTool()` can resolve with:\n * - MCP Apps surface: `{ structuredContent, content }`\n * - OpenAI surface: `{ result: \"<JSON string>\" }`, where the JSON may itself be\n * an MCP content array (`[{ type: 'text', text: \"<JSON string>\" }, ...]`)\n * - Fallback: the raw value returned by `callTool`\n *\n * The shape of the unwrapped payload is the tool's responsibility — this helper\n * does not project out `data` / `error` / `errors`. Callers read whichever keys\n * their tool contract defines.\n */\nexport function normalizeMcpResponse(raw: unknown): unknown {\n\tif (raw && typeof raw === \"object\" && \"structuredContent\" in raw) {\n\t\treturn (raw as { structuredContent: unknown }).structuredContent;\n\t}\n\n\tif (raw && typeof raw === \"object\" && typeof (raw as { result?: unknown }).result === \"string\") {\n\t\tconst parsed = JSON.parse((raw as { result: string }).result);\n\t\tif (Array.isArray(parsed)) {\n\t\t\tconst textBlock = parsed.find(\n\t\t\t\t(b: { type?: string; text?: string }) =>\n\t\t\t\t\tb && b.type === \"text\" && typeof b.text === \"string\",\n\t\t\t);\n\t\t\tconst text = textBlock ? textBlock.text : null;\n\t\t\tif (text) {\n\t\t\t\treturn JSON.parse(text);\n\t\t\t}\n\t\t\treturn {};\n\t\t}\n\t\treturn parsed;\n\t}\n\n\treturn raw ?? {};\n}\n","/**\n * Copyright (c) 2026, Salesforce, Inc.,\n * All rights reserved.\n * For full license text, see the LICENSE.txt file\n */\nimport { getChatSDK } from \"@salesforce/sdk-chat\";\nimport { normalizeMcpResponse } from \"../shared/normalize-mcp-response\";\n\n// Overwritten at plugin-load time by the `lightningGraphql` Vite shim, which\n// appends `TOOL_NAME = \"<configured-name>\";` to the bundled output. The initial\n// value is the default.\n// eslint-disable-next-line prefer-const\nexport let TOOL_NAME = \"graphqlQuery\";\n\ninterface GraphqlConfig {\n\tquery?: string;\n\tvariables?: Record<string, unknown>;\n}\n\ninterface GraphqlResult {\n\tdata?: unknown;\n\terrors?: { message: string }[];\n}\n\ntype WireCallback = (result: {\n\tdata: unknown;\n\terrors: { message: string }[] | undefined;\n\trefresh: () => Promise<void>;\n}) => void;\n\nexport function gql(strings: TemplateStringsArray, ...values: unknown[]): string {\n\tlet result = \"\";\n\tstrings.forEach((string, i) => {\n\t\tresult += string;\n\t\tif (i < values.length) result += String(values[i]);\n\t});\n\treturn result;\n}\n\nasync function runQuery(config: GraphqlConfig): Promise<GraphqlResult> {\n\tconst { query, variables } = config;\n\tif (!query) return { data: undefined, errors: undefined };\n\n\t// 1. UIBundle / local dev: use globalThis.__sfdc_sdk__.graphql if available
|
|
1
|
+
{"version":3,"file":"runtime.js","sources":["../../../src/providers/shared/normalize-mcp-response.ts","../../../src/providers/lightning-graphql/runtime.ts"],"sourcesContent":["/**\n * Copyright (c) 2026, Salesforce, Inc.,\n * All rights reserved.\n * For full license text, see the LICENSE.txt file\n */\n\n/**\n * Unwraps the MCP tool transport envelope and returns the tool's payload as-is.\n *\n * Handles the three surface shapes `sdk.callTool()` can resolve with:\n * - MCP Apps surface: `{ structuredContent, content }`\n * - OpenAI surface: `{ result: \"<JSON string>\" }`, where the JSON may itself be\n * an MCP content array (`[{ type: 'text', text: \"<JSON string>\" }, ...]`)\n * - Fallback: the raw value returned by `callTool`\n *\n * The shape of the unwrapped payload is the tool's responsibility — this helper\n * does not project out `data` / `error` / `errors`. Callers read whichever keys\n * their tool contract defines.\n */\nexport function normalizeMcpResponse(raw: unknown): unknown {\n\tif (raw && typeof raw === \"object\" && \"structuredContent\" in raw) {\n\t\treturn (raw as { structuredContent: unknown }).structuredContent;\n\t}\n\n\tif (raw && typeof raw === \"object\" && typeof (raw as { result?: unknown }).result === \"string\") {\n\t\tconst parsed = JSON.parse((raw as { result: string }).result);\n\t\tif (Array.isArray(parsed)) {\n\t\t\tconst textBlock = parsed.find(\n\t\t\t\t(b: { type?: string; text?: string }) =>\n\t\t\t\t\tb && b.type === \"text\" && typeof b.text === \"string\",\n\t\t\t);\n\t\t\tconst text = textBlock ? textBlock.text : null;\n\t\t\tif (text) {\n\t\t\t\treturn JSON.parse(text);\n\t\t\t}\n\t\t\treturn {};\n\t\t}\n\t\treturn parsed;\n\t}\n\n\treturn raw ?? {};\n}\n","/**\n * Copyright (c) 2026, Salesforce, Inc.,\n * All rights reserved.\n * For full license text, see the LICENSE.txt file\n */\nimport { getChatSDK } from \"@salesforce/sdk-chat\";\nimport { normalizeMcpResponse } from \"../shared/normalize-mcp-response\";\n\n// Overwritten at plugin-load time by the `lightningGraphql` Vite shim, which\n// appends `TOOL_NAME = \"<configured-name>\";` to the bundled output. The initial\n// value is the default.\n// eslint-disable-next-line prefer-const\nexport let TOOL_NAME = \"graphqlQuery\";\n\ninterface GraphqlConfig {\n\tquery?: string;\n\tvariables?: Record<string, unknown>;\n}\n\ninterface GraphqlResult {\n\tdata?: unknown;\n\terrors?: { message: string }[];\n}\n\ntype WireCallback = (result: {\n\tdata: unknown;\n\terrors: { message: string }[] | undefined;\n\trefresh: () => Promise<void>;\n}) => void;\n\nexport function gql(strings: TemplateStringsArray, ...values: unknown[]): string {\n\tlet result = \"\";\n\tstrings.forEach((string, i) => {\n\t\tresult += string;\n\t\tif (i < values.length) result += String(values[i]);\n\t});\n\treturn result;\n}\n\nasync function runQuery(config: GraphqlConfig): Promise<GraphqlResult> {\n\tconst { query, variables } = config;\n\tif (!query) return { data: undefined, errors: undefined };\n\n\t// 1. UIBundle / local dev: use globalThis.__sfdc_sdk__.graphql if available.\n\t// Invoke as a method (not a destructured reference) so class-based SDK\n\t// implementations keep their `this` binding — some SDKs implement\n\t// `graphql` as a method that calls `this.fetch(...)` internally.\n\tconst sdkRef = (globalThis as Record<string, unknown>).__sfdc_sdk__ as\n\t\t| {\n\t\t\t\tgraphql?: (args: {\n\t\t\t\t\tquery: string;\n\t\t\t\t\tvariables: Record<string, unknown>;\n\t\t\t\t}) => Promise<GraphqlResult>;\n\t\t }\n\t\t| undefined;\n\tif (sdkRef && typeof sdkRef.graphql === \"function\") {\n\t\tconst result = await sdkRef.graphql({ query, variables: variables ?? {} });\n\t\treturn { data: result?.data, errors: result?.errors };\n\t}\n\n\t// 2. MCP surface: use getChatSDK().callTool\n\tconst sdk = await getChatSDK();\n\tif (typeof sdk.callTool !== \"function\") {\n\t\tthrow new Error(\n\t\t\t\"[lightning/graphql] No data surface available. \" +\n\t\t\t\t\"Either initialise globalThis.__sfdc_sdk__ with createDataSDK, \" +\n\t\t\t\t\"or run inside a ChatGPT / MCP Apps context.\",\n\t\t);\n\t}\n\n\tconst raw = await sdk.callTool({\n\t\ttoolName: TOOL_NAME,\n\t\tparams: { query, variables: variables ?? {} },\n\t});\n\n\treturn (normalizeMcpResponse(raw) as GraphqlResult) ?? {};\n}\n\nexport class graphql {\n\t_dataCallback: WireCallback;\n\t_config: GraphqlConfig | undefined;\n\n\tconstructor(dataCallback: WireCallback) {\n\t\tthis._dataCallback = dataCallback;\n\t}\n\n\tconnect() {\n\t\tthis._fetch();\n\t}\n\n\t// eslint-disable-next-line @typescript-eslint/no-empty-function\n\tdisconnect() {}\n\n\tupdate(config: GraphqlConfig) {\n\t\tthis._config = config;\n\t\tthis._fetch();\n\t}\n\n\trefresh() {\n\t\treturn this._fetch();\n\t}\n\n\tasync _fetch() {\n\t\ttry {\n\t\t\tconst result = await runQuery(this._config ?? {});\n\t\t\tthis._emit(result);\n\t\t} catch (error) {\n\t\t\tthis._emit({\n\t\t\t\tdata: undefined,\n\t\t\t\terrors: [{ message: (error as Error).message }],\n\t\t\t});\n\t\t}\n\t}\n\n\t_emit({ data, errors }: GraphqlResult) {\n\t\tthis._dataCallback({\n\t\t\tdata,\n\t\t\terrors: errors?.length ? errors : undefined,\n\t\t\trefresh: () => this.refresh(),\n\t\t});\n\t}\n}\n\nexport async function executeMutation(config: GraphqlConfig): Promise<GraphqlResult> {\n\tif (!config?.query) return { data: undefined, errors: [{ message: \"No query provided\" }] };\n\ttry {\n\t\treturn await runQuery(config);\n\t} catch (error) {\n\t\treturn { data: undefined, errors: [{ message: (error as Error).message }] };\n\t}\n}\n"],"names":[],"mappings":";AAmBO,SAAS,qBAAqB,KAAuB;AAC3D,MAAI,OAAO,OAAO,QAAQ,YAAY,uBAAuB,KAAK;AACjE,WAAQ,IAAuC;AAAA,EAChD;AAEA,MAAI,OAAO,OAAO,QAAQ,YAAY,OAAQ,IAA6B,WAAW,UAAU;AAC/F,UAAM,SAAS,KAAK,MAAO,IAA2B,MAAM;AAC5D,QAAI,MAAM,QAAQ,MAAM,GAAG;AAC1B,YAAM,YAAY,OAAO;AAAA,QACxB,CAAC,MACA,KAAK,EAAE,SAAS,UAAU,OAAO,EAAE,SAAS;AAAA,MAAA;AAE9C,YAAM,OAAO,YAAY,UAAU,OAAO;AAC1C,UAAI,MAAM;AACT,eAAO,KAAK,MAAM,IAAI;AAAA,MACvB;AACA,aAAO,CAAA;AAAA,IACR;AACA,WAAO;AAAA,EACR;AAEA,SAAO,OAAO,CAAA;AACf;AC7BO,IAAI,YAAY;AAkBhB,SAAS,IAAI,YAAkC,QAA2B;AAChF,MAAI,SAAS;AACb,UAAQ,QAAQ,CAAC,QAAQ,MAAM;AAC9B,cAAU;AACV,QAAI,IAAI,OAAO,kBAAkB,OAAO,OAAO,CAAC,CAAC;AAAA,EAClD,CAAC;AACD,SAAO;AACR;AAEA,eAAe,SAAS,QAA+C;AACtE,QAAM,EAAE,OAAO,UAAA,IAAc;AAC7B,MAAI,CAAC,MAAO,QAAO,EAAE,MAAM,QAAW,QAAQ,OAAA;AAM9C,QAAM,SAAU,WAAuC;AAQvD,MAAI,UAAU,OAAO,OAAO,YAAY,YAAY;AACnD,UAAM,SAAS,MAAM,OAAO,QAAQ,EAAE,OAAO,WAAW,aAAa,CAAA,GAAI;AACzE,WAAO,EAAE,MAAM,QAAQ,MAAM,QAAQ,QAAQ,OAAA;AAAA,EAC9C;AAGA,QAAM,MAAM,MAAM,WAAA;AAClB,MAAI,OAAO,IAAI,aAAa,YAAY;AACvC,UAAM,IAAI;AAAA,MACT;AAAA,IAAA;AAAA,EAIF;AAEA,QAAM,MAAM,MAAM,IAAI,SAAS;AAAA,IAC9B,UAAU;AAAA,IACV,QAAQ,EAAE,OAAO,WAAW,aAAa,CAAA,EAAC;AAAA,EAAE,CAC5C;AAED,SAAQ,qBAAqB,GAAG,KAAuB,CAAA;AACxD;AAEO,MAAM,QAAQ;AAAA,EACpB;AAAA,EACA;AAAA,EAEA,YAAY,cAA4B;AACvC,SAAK,gBAAgB;AAAA,EACtB;AAAA,EAEA,UAAU;AACT,SAAK,OAAA;AAAA,EACN;AAAA;AAAA,EAGA,aAAa;AAAA,EAAC;AAAA,EAEd,OAAO,QAAuB;AAC7B,SAAK,UAAU;AACf,SAAK,OAAA;AAAA,EACN;AAAA,EAEA,UAAU;AACT,WAAO,KAAK,OAAA;AAAA,EACb;AAAA,EAEA,MAAM,SAAS;AACd,QAAI;AACH,YAAM,SAAS,MAAM,SAAS,KAAK,WAAW,CAAA,CAAE;AAChD,WAAK,MAAM,MAAM;AAAA,IAClB,SAAS,OAAO;AACf,WAAK,MAAM;AAAA,QACV,MAAM;AAAA,QACN,QAAQ,CAAC,EAAE,SAAU,MAAgB,SAAS;AAAA,MAAA,CAC9C;AAAA,IACF;AAAA,EACD;AAAA,EAEA,MAAM,EAAE,MAAM,UAAyB;AACtC,SAAK,cAAc;AAAA,MAClB;AAAA,MACA,QAAQ,QAAQ,SAAS,SAAS;AAAA,MAClC,SAAS,MAAM,KAAK,QAAA;AAAA,IAAQ,CAC5B;AAAA,EACF;AACD;AAEA,eAAsB,gBAAgB,QAA+C;AACpF,MAAI,CAAC,QAAQ,MAAO,QAAO,EAAE,MAAM,QAAW,QAAQ,CAAC,EAAE,SAAS,oBAAA,CAAqB,EAAA;AACvF,MAAI;AACH,WAAO,MAAM,SAAS,MAAM;AAAA,EAC7B,SAAS,OAAO;AACf,WAAO,EAAE,MAAM,QAAW,QAAQ,CAAC,EAAE,SAAU,MAAgB,QAAA,CAAS,EAAA;AAAA,EACzE;AACD;"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforce/vite-plugin-lwc-ui-bundle",
|
|
3
|
-
"version": "1.133.
|
|
3
|
+
"version": "1.133.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",
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"magic-string": "^0.30.17"
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
|
-
"@salesforce/sdk-chat": "^1.133.
|
|
76
|
+
"@salesforce/sdk-chat": "^1.133.2",
|
|
77
77
|
"typescript": "^5.9.3",
|
|
78
78
|
"vite": "^7.0.0",
|
|
79
79
|
"vite-plugin-dts": "^4.5.4",
|
|
@@ -12,123 +12,237 @@ description: >
|
|
|
12
12
|
|
|
13
13
|
# Setup @salesforce/vite-plugin-lwc-ui-bundle
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
producing a single `dist/index.html` that runs in
|
|
15
|
+
Adds `@salesforce/vite-plugin-lwc-ui-bundle` to an existing LWC project,
|
|
16
|
+
producing a single `dist/index.html` that runs in a browser without a
|
|
17
|
+
Salesforce org.
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
and the Vite/LWC bridge. The output is a self-contained HTML file with all JS and CSS
|
|
23
|
-
inlined.
|
|
19
|
+
The plugin wraps the full LWC compilation pipeline behind one Vite plugin:
|
|
20
|
+
scoped module providers (labels, i18n, gates, etc.), Lightning npm
|
|
21
|
+
resolution, missing CSS handling, and the Vite/LWC bridge. Output is a
|
|
22
|
+
self-contained HTML file with all JS and CSS inlined.
|
|
24
23
|
|
|
25
24
|
## Reference material
|
|
26
25
|
|
|
27
|
-
The
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
The skill dispatches to these reference files. Read each one when the
|
|
27
|
+
relevant step arrives — don't try to inline everything from here:
|
|
28
|
+
|
|
29
|
+
- **`references/consumer-guide.md`** — exact file templates (`vite.config.js`,
|
|
30
|
+
`index.html`, `bootstrap.js`, `package.json`), dependency list, and
|
|
31
|
+
troubleshooting. This is the source of truth for generated files.
|
|
32
|
+
- **`references/chat-wrapper-flow.md`** — the full sub-flow for Step 2 (ask
|
|
33
|
+
about chat wrappers, analyze `@api` surface, confirm mapping, generate
|
|
34
|
+
`chatContextAdapter` + `<component>ChatMapper` + `<component>ChatWrapper`).
|
|
35
|
+
- **`references/bootstrap-js-patterns.md`** — the conditional `callTool`
|
|
36
|
+
shim skeleton, envelope/tool-contract rules, how to handle an existing
|
|
37
|
+
`bootstrap.js`, and why mocks only work in `npm run dev`.
|
|
38
|
+
- **`references/known-pitfalls.md`** — consolidated list of real bugs seen
|
|
39
|
+
in consumer projects, each with symptom / cause / fix. Consult when a
|
|
40
|
+
build or runtime fails with a confusing message.
|
|
31
41
|
|
|
32
|
-
## Interactive
|
|
42
|
+
## Interactive setup flow
|
|
33
43
|
|
|
34
|
-
Walk
|
|
44
|
+
Walk through these steps in order. Ask questions — don't assume. When a
|
|
45
|
+
step points to a reference file, read it before proceeding.
|
|
35
46
|
|
|
36
47
|
### Step 1: Detect the project
|
|
37
48
|
|
|
38
|
-
LWC projects come in two layouts, and the directory structure determines
|
|
39
|
-
`modules.dirs` must be configured. Detecting the layout first avoids
|
|
40
|
-
imports that silently fail at build time.
|
|
49
|
+
LWC projects come in two layouts, and the directory structure determines
|
|
50
|
+
how `modules.dirs` must be configured. Detecting the layout first avoids
|
|
51
|
+
misconfigured imports that silently fail at build time.
|
|
41
52
|
|
|
42
53
|
Check in order:
|
|
43
54
|
|
|
44
|
-
1. `sfdx-project.json` in the project root → **SFDX project**. Components
|
|
45
|
-
`force-app/main/default/lwc/` in a flat structure where every
|
|
46
|
-
the `c` namespace. Configure as
|
|
47
|
-
|
|
48
|
-
|
|
55
|
+
1. `sfdx-project.json` in the project root → **SFDX project**. Components
|
|
56
|
+
live at `force-app/main/default/lwc/` in a flat structure where every
|
|
57
|
+
component shares the `c` namespace. Configure as
|
|
58
|
+
`dirs: [{ path: "force-app/main/default/lwc", namespace: "c" }]`.
|
|
59
|
+
2. Directories matching `src/lwc/` or `modules/` → **off-core project**.
|
|
60
|
+
Sub-directories are namespaces (e.g., `src/lwc/myNs/myComponent/`).
|
|
61
|
+
Configure as `dirs: ["src/lwc"]`.
|
|
49
62
|
3. If neither found, ask the user where their LWC components live.
|
|
50
63
|
|
|
51
|
-
Read `package.json` to understand what's already installed — avoid
|
|
52
|
-
|
|
64
|
+
Read `package.json` to understand what's already installed — avoid
|
|
65
|
+
duplicates later. If the project already has `vite.config.js`,
|
|
66
|
+
`bootstrap.js`, or `main.js`, read them too so the later steps can merge
|
|
67
|
+
instead of overwriting.
|
|
68
|
+
|
|
69
|
+
Check the user's Node.js version (`node --version`). Vite 8.x requires
|
|
70
|
+
Node `^20.19.0 || >=22.12.0`. On older Node 20.x, builds fail deep inside
|
|
71
|
+
rolldown with "Cannot find native binding" (see `known-pitfalls.md#1`).
|
|
72
|
+
Surface this early:
|
|
73
|
+
|
|
74
|
+
> "You're on Node 20.14.0. Vite 8 needs 20.19+. Options:
|
|
75
|
+
> A) Upgrade Node (recommended — `nvm install 20.19` or `22`)
|
|
76
|
+
> B) Pin `vite@^7` and a compatible `@lwc/rollup-plugin` major"
|
|
53
77
|
|
|
54
|
-
### Step 2: Ask
|
|
78
|
+
### Step 2: Ask about chat wrappers
|
|
79
|
+
|
|
80
|
+
If the project will run inside a ChatGPT/MCP host (tool output drives
|
|
81
|
+
the UI), components need a thin chat-aware wrapper + mapper. The wrapper
|
|
82
|
+
reads tool output from the host, the mapper normalizes it into the
|
|
83
|
+
component's `@api` props, and the component itself stays unchanged.
|
|
84
|
+
|
|
85
|
+
Ask before proceeding to root-component selection:
|
|
86
|
+
|
|
87
|
+
> "Will any component in this project need to be driven by ChatGPT/MCP
|
|
88
|
+
> tool output? I can generate a chat wrapper + mapper for each one so the
|
|
89
|
+
> component stays unchanged and the wrapper handles host integration.
|
|
90
|
+
>
|
|
91
|
+
> A) Yes — walk me through creating a wrapper
|
|
92
|
+
> B) No — skip this step"
|
|
55
93
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
94
|
+
If **B**, proceed directly to Step 3.
|
|
95
|
+
|
|
96
|
+
If **A**, read `references/chat-wrapper-flow.md` and walk through its
|
|
97
|
+
sub-steps 2a–2e for each component the user wants to wrap.
|
|
98
|
+
|
|
99
|
+
### Step 3: Pick the root component
|
|
100
|
+
|
|
101
|
+
The plugin compiles a tree starting from one root component that gets
|
|
102
|
+
mounted in the HTML page. The user needs to tell you which one — there's
|
|
103
|
+
no reliable way to auto-detect the "main" component.
|
|
59
104
|
|
|
60
105
|
List the discovered components and ask:
|
|
61
106
|
|
|
62
|
-
> "Which component should be the root of your app? This is the one that
|
|
63
|
-
> mounted in the HTML page."
|
|
107
|
+
> "Which component should be the root of your app? This is the one that
|
|
108
|
+
> gets mounted in the HTML page."
|
|
64
109
|
|
|
65
|
-
Present the
|
|
110
|
+
Present the names as a numbered list for easy selection.
|
|
66
111
|
|
|
67
|
-
### Step
|
|
112
|
+
### Step 4: Inspect the component tree
|
|
68
113
|
|
|
69
|
-
This step drives every downstream decision: which providers to include,
|
|
70
|
-
`lightning-base-components`
|
|
71
|
-
this right prevents cryptic build
|
|
114
|
+
This step drives every downstream decision: which providers to include,
|
|
115
|
+
which `lightning-base-components` are needed, and which tools need mock
|
|
116
|
+
branches for local dev. Getting this right prevents cryptic build
|
|
117
|
+
errors.
|
|
72
118
|
|
|
73
119
|
Starting from the root component, trace the dependency tree:
|
|
74
120
|
|
|
75
121
|
1. Read the root component's `.html` file — find all `c-*` tags (or other
|
|
76
|
-
namespace tags) to identify child components
|
|
77
|
-
2. Recursively read each child component's `.js` and `.html
|
|
122
|
+
namespace tags) to identify child components.
|
|
123
|
+
2. Recursively read each child component's `.js` and `.html`.
|
|
78
124
|
3. Collect all imports across the tree:
|
|
79
125
|
- `@salesforce/label/*` → label keys (need `builtins.label()`)
|
|
80
|
-
- `lightning/graphql`
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
126
|
+
- `lightning/graphql` AND `@wire(graphql` present → needs GraphQL
|
|
127
|
+
support (provider + mock branch)
|
|
128
|
+
- `lightning/uiRecordApi` imported AND `@wire(getRecord` / `@wire(get*`
|
|
129
|
+
actually used → needs LDS support (provider + mock branch)
|
|
130
|
+
- `lightning/*` base components → needs `lightning-base-components`
|
|
131
|
+
npm package, plus `gate()`, `accessCheck()`, and `primitiveUtils()`
|
|
132
|
+
providers because base components use these modules internally even
|
|
133
|
+
if user code doesn't
|
|
134
|
+
- `@salesforce/gate/*`, `@salesforce/accessCheck/*` → handled by
|
|
135
|
+
those providers
|
|
136
|
+
- `@salesforce/i18n/*` → `i18n` provider
|
|
137
|
+
- `@salesforce/client/*` → `client` provider (provides `formFactor`
|
|
138
|
+
based on viewport width)
|
|
139
|
+
- `lightning/primitiveUtils` → `primitiveUtils` provider
|
|
140
|
+
|
|
141
|
+
**Be precise with LDS detection.** The LDS provider/mock is only needed
|
|
142
|
+
when a component's `@wire` actually consumes `getRecord`/`getRecordUi`/
|
|
143
|
+
similar adapters from `lightning/uiRecordApi`. **Do not trigger the LDS
|
|
144
|
+
provider just because:**
|
|
145
|
+
|
|
146
|
+
- The app has a chat wrapper whose tool output happens to contain a
|
|
147
|
+
record shape (`records[...]`) — that goes through
|
|
148
|
+
`window.openai.toolOutput` + the mapper, not the LDS wire adapter.
|
|
149
|
+
- A component imports `lightning/uiRecordApi` but doesn't `@wire` it.
|
|
150
|
+
|
|
151
|
+
`builtins.lds()` wires up `@wire(getRecord)` specifically. If no
|
|
152
|
+
component calls those wire adapters, omit both the provider and the
|
|
153
|
+
`getRecordMcpTool` mock branch. A chat wrapper that maps
|
|
154
|
+
`window.openai.toolOutput.records[...]` into a component's `@api`
|
|
155
|
+
props is independent of LDS.
|
|
156
|
+
|
|
157
|
+
Report what you found — plainly, so the user can correct misdetections:
|
|
90
158
|
|
|
91
159
|
> "I traced your component tree from `c/app`. Here's what I found:
|
|
92
160
|
>
|
|
93
161
|
> - 5 components total
|
|
94
162
|
> - 3 label imports: `c.appTitle`, `c.greeting`, `c.save`
|
|
95
|
-
> - Uses `lightning-card`
|
|
96
|
-
>
|
|
97
|
-
> -
|
|
98
|
-
>
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
|
107
|
-
|
|
|
108
|
-
| **
|
|
163
|
+
> - Uses `lightning-card` → needs lightning-base-components + the
|
|
164
|
+
> gate/accessCheck/primitiveUtils providers
|
|
165
|
+
> - Uses `lightning/graphql` → needs `builtins.lightningGraphql()` + a
|
|
166
|
+
> graphql mock branch in `bootstrap.js`
|
|
167
|
+
> - Uses `lightning/uiRecordApi` → needs `builtins.lds()` + a
|
|
168
|
+
> `getRecordMcpTool` mock branch"
|
|
169
|
+
|
|
170
|
+
### Step 5: Decide the runtime mode
|
|
171
|
+
|
|
172
|
+
Two runtime contexts, one compiled bundle:
|
|
173
|
+
|
|
174
|
+
| Context | Data source | Notes |
|
|
175
|
+
| ---------------------- | -------------------------------------------------------- | ------------------------------------------------------------- |
|
|
176
|
+
| **Local dev** | Mock `window.openai` shim in `bootstrap.js` | `npm run dev` only. No org, no ChatGPT host. |
|
|
177
|
+
| **ChatGPT / MCP host** | Host-provided `window.openai` (`callTool`, `toolOutput`) | Production. Same compiled bundle; mocks skipped by the guard. |
|
|
178
|
+
|
|
179
|
+
The `if (!window.openai?.callTool)` guard is what makes one bundle work
|
|
180
|
+
in both contexts. ChatGPT/MCP hosts set `window.openai` before loading
|
|
181
|
+
the bundle, so the guard short-circuits.
|
|
182
|
+
|
|
183
|
+
> `lwcProxy()` is intentionally not offered. It doesn't currently support
|
|
184
|
+
> `lightning/uiRecordApi`, so it's not a reliable local-dev substitute
|
|
185
|
+
> for components that use LDS. Keep the workflow simple: mock locally,
|
|
186
|
+
> real host in ChatGPT.
|
|
187
|
+
|
|
188
|
+
Ask the user whether to generate mocks. Mocks serve two independent
|
|
189
|
+
purposes — list each one **only if Step 4 / Step 2 produced something
|
|
190
|
+
to mock**, and skip lines that don't apply:
|
|
191
|
+
|
|
192
|
+
- **`callTool` branches for wire adapters** — one branch per wire
|
|
193
|
+
provider actually detected in Step 4:
|
|
194
|
+
- `graphqlQuery` — only if Step 4 found `@wire(graphql)` in a
|
|
195
|
+
component's `.js`. Not needed otherwise.
|
|
196
|
+
- `getRecordMcpTool` — only if Step 4 found `@wire(getRecord)` in
|
|
197
|
+
a component's `.js`. Not needed just because a chat wrapper
|
|
198
|
+
passes record-shaped data; the wrapper reads `toolOutput`, not
|
|
199
|
+
`callTool`.
|
|
200
|
+
- **Seed `window.openai.toolOutput`** — only if Step 2 generated a
|
|
201
|
+
chat wrapper. This is what drives the wrapper's initial render; the
|
|
202
|
+
mapper consumes it. It has nothing to do with `callTool`.
|
|
203
|
+
|
|
204
|
+
Build the question by combining whichever applies. For example, a
|
|
205
|
+
project with graphql only:
|
|
206
|
+
|
|
207
|
+
> "For local dev (`npm run dev`) I can add mocks in `bootstrap.js`. The
|
|
208
|
+
> shim only activates when no host bridge is present, so the same
|
|
209
|
+
> compiled bundle runs inside ChatGPT unchanged.
|
|
210
|
+
>
|
|
211
|
+
> Based on Step 4, the mock will need:
|
|
212
|
+
>
|
|
213
|
+
> - `callTool` branch for `graphqlQuery`
|
|
214
|
+
>
|
|
215
|
+
> A) Yes — generate mocks with sample data I'll describe
|
|
216
|
+
> B) No — skip mocks
|
|
217
|
+
>
|
|
218
|
+
> If B, `@wire(graphql)` will return no data under `npm run dev`, but
|
|
219
|
+
> the bundle will still render correctly when deployed to a real MCP
|
|
220
|
+
> server as a UI resource — the host provides a real `window.openai`
|
|
221
|
+
> there."
|
|
109
222
|
|
|
110
|
-
|
|
223
|
+
For a project with a chat wrapper **and** `@wire(graphql)`, include
|
|
224
|
+
both the `callTool` branch and the `toolOutput` seed, but keep them
|
|
225
|
+
as distinct items in the list:
|
|
111
226
|
|
|
112
|
-
>
|
|
113
|
-
> Salesforce org for live data during development?
|
|
227
|
+
> Based on Step 2 / Step 4, the mocks will include:
|
|
114
228
|
>
|
|
115
|
-
>
|
|
116
|
-
>
|
|
229
|
+
> - `callTool` branch for `graphqlQuery` (for the `@wire(graphql)` call)
|
|
230
|
+
> - Seed for `window.openai.toolOutput` (drives the
|
|
231
|
+
> `<bcx-record-detail-chat-wrapper>` initial render)
|
|
117
232
|
|
|
118
|
-
|
|
119
|
-
GraphQL or live data needs.
|
|
233
|
+
### Step 6: Resolve label values
|
|
120
234
|
|
|
121
|
-
|
|
235
|
+
Labels need explicit values because the plugin can't read them from an
|
|
236
|
+
org at build time. Getting them right here means the user sees realistic
|
|
237
|
+
text immediately instead of placeholder keys.
|
|
122
238
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
1. **SFDX project**: Check for `force-app/main/default/labels/CustomLabels.labels-meta.xml`.
|
|
128
|
-
If found, parse it and extract `<fullName>` → `<value>` pairs for the label
|
|
129
|
-
keys discovered in Step 3.
|
|
239
|
+
1. **SFDX project**: Check for
|
|
240
|
+
`force-app/main/default/labels/CustomLabels.labels-meta.xml`. If found,
|
|
241
|
+
parse it and extract `<fullName>` → `<value>` pairs for the label keys
|
|
242
|
+
discovered in Step 4.
|
|
130
243
|
2. **Off-core project or no labels XML**: For each label key found, use a
|
|
131
|
-
placeholder value derived from the key (e.g., `c.appTitle` → `"App
|
|
244
|
+
placeholder value derived from the key (e.g., `c.appTitle` → `"App
|
|
245
|
+
Title"`).
|
|
132
246
|
|
|
133
247
|
Tell the user what labels you found and the values you'll use:
|
|
134
248
|
|
|
@@ -140,103 +254,186 @@ Tell the user what labels you found and the values you'll use:
|
|
|
140
254
|
>
|
|
141
255
|
> You can change these in `vite.config.js` later."
|
|
142
256
|
|
|
143
|
-
If no labels were found at all, still include `builtins.label()` with no
|
|
144
|
-
The plugin returns a human-readable fallback for unknown keys,
|
|
145
|
-
`lightning-base-components` may use labels internally.
|
|
257
|
+
If no labels were found at all, still include `builtins.label()` with no
|
|
258
|
+
overrides. The plugin returns a human-readable fallback for unknown keys,
|
|
259
|
+
and `lightning-base-components` may use labels internally.
|
|
260
|
+
|
|
261
|
+
### Step 7: Generate or update files
|
|
262
|
+
|
|
263
|
+
Read `references/consumer-guide.md` for the exact file templates. For
|
|
264
|
+
each file below, check whether it already exists:
|
|
265
|
+
|
|
266
|
+
- **If it exists**: read it first, then merge changes instead of
|
|
267
|
+
overwriting. Ask the user before making destructive edits to
|
|
268
|
+
hand-written sections.
|
|
269
|
+
- **If it doesn't exist**: generate from the consumer-guide template,
|
|
270
|
+
adapted to what Steps 1–6 found.
|
|
271
|
+
|
|
272
|
+
#### `package.json`
|
|
273
|
+
|
|
274
|
+
Merge in dependencies and scripts. Don't overwrite existing.
|
|
275
|
+
|
|
276
|
+
- Add `"type": "module"` if not present.
|
|
277
|
+
- Only add `lightning-base-components` and `@salesforce-ux/design-system`
|
|
278
|
+
if the component tree uses `lightning/*` base components.
|
|
279
|
+
- **Do not hardcode versions from the consumer guide.** Pinned versions
|
|
280
|
+
drift out of date (prerelease / alpha tags get unpublished). Look up
|
|
281
|
+
the current version with `npm view <pkg> version` and pin using
|
|
282
|
+
caret-major. At minimum, look up:
|
|
283
|
+
- `@salesforce/vite-plugin-lwc-ui-bundle`
|
|
284
|
+
- `@lwc/rollup-plugin`
|
|
285
|
+
- `lwc`
|
|
286
|
+
- `lightning-base-components` (if used)
|
|
287
|
+
- `@salesforce-ux/design-system` (if used)
|
|
288
|
+
- `vite`
|
|
289
|
+
- `vite-plugin-singlefile`
|
|
290
|
+
|
|
291
|
+
If a registry lookup fails (offline / auth issue), ask the user for
|
|
292
|
+
the version rather than falling back to the stale snippet.
|
|
293
|
+
|
|
294
|
+
#### `vite.config.js`
|
|
295
|
+
|
|
296
|
+
See "Step 2" in the consumer guide for the template.
|
|
297
|
+
|
|
298
|
+
**Import providers from the plugin, do not reimplement them.** All
|
|
299
|
+
provider names (`label`, `i18n`, `client`, `gate`, `accessCheck`,
|
|
300
|
+
`primitiveUtils`, `lightningGraphql`, `lds`) must come from the `builtins`
|
|
301
|
+
export of `@salesforce/vite-plugin-lwc-ui-bundle`. Calling them invokes
|
|
302
|
+
the plugin's real runtime — which includes
|
|
303
|
+
`lightning/graphql` wire adapter, `normalizeMcpResponse` envelope
|
|
304
|
+
parsing, LDS wire adapter, and more. Hand-rolling any of them bypasses
|
|
305
|
+
all that and breaks host integration (see
|
|
306
|
+
`references/known-pitfalls.md#3`).
|
|
307
|
+
|
|
308
|
+
```js
|
|
309
|
+
import lwcVitePlugin, { builtins } from "@salesforce/vite-plugin-lwc-ui-bundle";
|
|
310
|
+
|
|
311
|
+
// inside plugins -> lwcVitePlugin({ providers: [...] })
|
|
312
|
+
builtins.lightningGraphql(), // default tool name: "graphqlQuery"
|
|
313
|
+
builtins.lightningGraphql({ toolName: "yourCustomTool" }), // override
|
|
314
|
+
builtins.lds(), // default registry
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Adapt based on earlier findings:
|
|
146
318
|
|
|
147
|
-
|
|
319
|
+
- Set `dirs` for the detected project structure (SFDX vs namespaced).
|
|
320
|
+
- Populate `builtins.label({...})` with values from Step 6.
|
|
321
|
+
- Include `builtins.primitiveUtils()` if using `lightning-base-components`.
|
|
322
|
+
- Include `builtins.lightningGraphql()` only if GraphQL was detected.
|
|
323
|
+
- Include `builtins.lds()` if any component uses `lightning/uiRecordApi`.
|
|
324
|
+
- Include `viteSingleFile()` — without it, `vite build` emits multi-file
|
|
325
|
+
output and the MCP host can't inline the bundle. See
|
|
326
|
+
`known-pitfalls.md#7` for the full config block.
|
|
327
|
+
- Remove `npm: ["lightning-base-components"]` if no base components used.
|
|
148
328
|
|
|
149
|
-
|
|
150
|
-
files, adapting the templates based on what you learned in Steps 1-5:
|
|
329
|
+
**Tool names** (canonical list — referenced elsewhere):
|
|
151
330
|
|
|
152
|
-
|
|
331
|
+
- `builtins.lds()` default for `lightning/uiRecordApi.getRecord`:
|
|
332
|
+
`getRecordMcpTool`. Override via
|
|
333
|
+
`builtins.lds({ "lightning/uiRecordApi": { getRecord: { toolName: "..." } } })`.
|
|
334
|
+
- `builtins.lightningGraphql()` default: `graphqlQuery` (**not**
|
|
335
|
+
`"graphql"` or `"lightningGraphql"`). Override via
|
|
336
|
+
`builtins.lightningGraphql({ toolName: "..." })`.
|
|
153
337
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
- Add `"type": "module"` if not present
|
|
157
|
-
- Only add `lightning-base-components` and `@salesforce-ux/design-system` if
|
|
158
|
-
the component tree uses `lightning/*` base components
|
|
159
|
-
- For Tier 2, also add `@salesforce/sdk-data` and `@salesforce/ui-bundle`
|
|
338
|
+
Whatever tool names you settle on here **must match** the branches in
|
|
339
|
+
`bootstrap.js` mocks (Step 8).
|
|
160
340
|
|
|
161
|
-
|
|
162
|
-
Adapt based on your findings:
|
|
163
|
-
- Set `dirs` for the detected project structure (SFDX vs namespaced)
|
|
164
|
-
- Populate `builtins.label({...})` with values from Step 5
|
|
165
|
-
- Include `builtins.primitiveUtils()` if using `lightning-base-components`
|
|
166
|
-
- Include `builtins.lightningGraphql()` only if GraphQL was detected
|
|
167
|
-
- Remove `npm: ["lightning-base-components"]` if no base components used
|
|
168
|
-
- For Tier 2, add `lwcProxy()` before `lwcVitePlugin()` in the plugins array
|
|
341
|
+
#### `index.html`
|
|
169
342
|
|
|
170
|
-
|
|
343
|
+
See "Step 3" in the consumer guide.
|
|
171
344
|
|
|
172
|
-
|
|
173
|
-
in the consumer guide. Replace the component name with the actual root from
|
|
174
|
-
Step 2. Remove the SLDS CSS import if `lightning-base-components` is not used.
|
|
345
|
+
#### `bootstrap.js`
|
|
175
346
|
|
|
176
|
-
|
|
347
|
+
This file has its own dedicated reference. Read
|
|
348
|
+
`references/bootstrap-js-patterns.md` now for:
|
|
177
349
|
|
|
178
|
-
The
|
|
179
|
-
|
|
350
|
+
- The skeleton structure (mock data → conditional shim → dynamic imports).
|
|
351
|
+
- The tool-contract / envelope rules (what shape each mock branch must
|
|
352
|
+
return, and why double-wrapping breaks the graphql wire adapter).
|
|
353
|
+
- Guidance on merging into an existing `bootstrap.js` or `main.js`.
|
|
354
|
+
- Why production builds don't work with mocks (rolldown module-init
|
|
355
|
+
order defeats the shim; mocks are dev-server only).
|
|
180
356
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
Needed when components import platform modules like `aura`, `logger`, or
|
|
184
|
-
`force/*` that don't exist outside Salesforce.
|
|
185
|
-
- **`lwcOptions`**: Pass-through options for `@lwc/rollup-plugin` to configure
|
|
186
|
-
LWC compiler behavior (e.g., `{ enableDynamicComponents: true }`). Only needed
|
|
187
|
-
for projects using advanced LWC features like dynamic component creation.
|
|
188
|
-
- **`passthroughRules`**: Let specific imports bypass the provider system. Each
|
|
189
|
-
rule has a `specifierPrefix` and `importerPattern` — when both match, the import
|
|
190
|
-
resolves normally. Useful when an npm package (like `lightning-base-components`)
|
|
191
|
-
has its own label definitions that shouldn't be intercepted by your label provider.
|
|
192
|
-
- **`ignorePatterns`**: Specifier prefixes that providers should never intercept.
|
|
193
|
-
Defaults include `@salesforce/sdk-*` and `@salesforce/core`. Extend this if
|
|
194
|
-
you have imports that look like provider-handled patterns but should resolve
|
|
195
|
-
through normal Node module resolution.
|
|
357
|
+
Use that reference to write the file; don't reconstruct the skeleton
|
|
358
|
+
from memory.
|
|
196
359
|
|
|
197
|
-
|
|
360
|
+
If the user chose **B** in Step 5 (no mocks), skip the shim entirely.
|
|
361
|
+
`@wire(graphql)` / `@wire(getRecord)` will return no data under
|
|
362
|
+
`npm run dev`, but the bundle will render correctly once deployed to a
|
|
363
|
+
real MCP server as a UI resource — the host provides `window.openai`
|
|
364
|
+
there.
|
|
198
365
|
|
|
199
|
-
|
|
366
|
+
#### Advanced plugin options
|
|
367
|
+
|
|
368
|
+
Only add these if the component tree inspection reveals a need:
|
|
369
|
+
|
|
370
|
+
- **`stubs`**: Map bare module specifiers to stub files for core-only
|
|
371
|
+
modules not available off-platform (e.g., `{ "force/someModule":
|
|
372
|
+
"./stubs/someModule.js" }`). Needed when components import platform
|
|
373
|
+
modules like `aura`, `logger`, or `force/*`.
|
|
374
|
+
- **`lwcOptions`**: Pass-through options for `@lwc/rollup-plugin`.
|
|
375
|
+
Only for advanced LWC features like dynamic component creation.
|
|
376
|
+
- **`passthroughRules`**: Let specific imports bypass the provider
|
|
377
|
+
system. Useful when an npm package has its own label definitions that
|
|
378
|
+
shouldn't be intercepted.
|
|
379
|
+
- **`ignorePatterns`**: Specifier prefixes that providers should never
|
|
380
|
+
intercept. Defaults include `@salesforce/sdk-*` and `@salesforce/core`.
|
|
381
|
+
|
|
382
|
+
### Step 8: Install and build
|
|
200
383
|
|
|
201
384
|
```bash
|
|
202
385
|
npm install
|
|
203
386
|
npm run build
|
|
204
387
|
```
|
|
205
388
|
|
|
206
|
-
If the build succeeds, report the output file size
|
|
389
|
+
If the build succeeds, report the output file size:
|
|
207
390
|
|
|
208
391
|
```bash
|
|
209
392
|
open dist/index.html
|
|
210
393
|
```
|
|
211
394
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
395
|
+
A correctly built `dist/index.html` is typically 100 KB+. A 1-2 KB file
|
|
396
|
+
is the un-inlined stub (see `known-pitfalls.md#7`).
|
|
397
|
+
|
|
398
|
+
If the build fails, diagnose via `references/known-pitfalls.md`. The most
|
|
399
|
+
frequent issues have symptoms that map directly to specific entries:
|
|
215
400
|
|
|
216
|
-
-
|
|
217
|
-
|
|
218
|
-
-
|
|
219
|
-
|
|
220
|
-
-
|
|
221
|
-
|
|
222
|
-
- **`Top-level await is not available`**: Missing `target: "esnext"` in build config.
|
|
223
|
-
- **`Rollup failed to resolve import "force/someModule"`**: Core-only module. Add
|
|
224
|
-
a stub via the `stubs` option.
|
|
225
|
-
- **Component renders but looks unstyled**: Missing SLDS CSS import in bootstrap.js.
|
|
401
|
+
- "Cannot find native binding" inside rolldown → pitfall #1 (Node)
|
|
402
|
+
- "No matching version found for \<pkg\>" → pitfall #2 (stale dep pins)
|
|
403
|
+
- Missing `isOpen` / missing `lightning/button` → pitfalls #9 / #7
|
|
404
|
+
- `force/someModule` not resolved → pitfall #10
|
|
405
|
+
- Component renders but unstyled → missing SLDS CSS import in
|
|
406
|
+
`bootstrap.js`
|
|
226
407
|
|
|
227
|
-
### Step
|
|
408
|
+
### Step 9: Verify local dev run
|
|
228
409
|
|
|
229
|
-
|
|
410
|
+
Start the dev server:
|
|
230
411
|
|
|
231
412
|
```bash
|
|
232
413
|
npm run dev
|
|
233
414
|
```
|
|
234
415
|
|
|
235
|
-
|
|
236
|
-
|
|
416
|
+
Open `http://localhost:5173` and confirm:
|
|
417
|
+
|
|
418
|
+
- The root component renders.
|
|
419
|
+
- If `bootstrap.js` has mocks, the wrappers show mapped data and wire
|
|
420
|
+
adapters return the mock payloads.
|
|
421
|
+
- If no mocks were generated, chat wrappers show "Waiting for tool
|
|
422
|
+
output..." and `@wire` adapters return no data. That's expected; the
|
|
423
|
+
bundle will work once deployed to a real MCP server.
|
|
424
|
+
|
|
425
|
+
If something renders wrong, check `references/known-pitfalls.md`. The
|
|
426
|
+
most common dev-time issues are graphql wire adapters returning empty
|
|
427
|
+
data (pitfalls #3, #4, #5) and mapper returning `null` on Avro-wrapped
|
|
428
|
+
payloads (#6).
|
|
429
|
+
|
|
430
|
+
The same compiled `dist/index.html` runs in ChatGPT — the guard in
|
|
431
|
+
`bootstrap.js` ensures local mocks are skipped when the host provides
|
|
432
|
+
the real bridge.
|
|
237
433
|
|
|
238
434
|
## Reference
|
|
239
435
|
|
|
240
436
|
- npm: https://www.npmjs.com/package/@salesforce/vite-plugin-lwc-ui-bundle
|
|
241
437
|
- README: https://github.com/salesforce-experience-platform-emu/webapps/tree/main/packages/vite-plugin-lwc-ui-bundle
|
|
242
438
|
- Consumer Guide: https://github.com/salesforce-experience-platform-emu/webapps/blob/main/packages/vite-plugin-lwc-ui-bundle/docs/consumer-guide.md
|
|
439
|
+
- Chat Wrapper Guide: https://github.com/salesforce-experience-platform-emu/webapps/blob/main/packages/vite-plugin-lwc-ui-bundle/docs/chat-wrapper-guide.md
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# `bootstrap.js` Patterns
|
|
2
|
+
|
|
3
|
+
Read this reference when writing or editing the project's `bootstrap.js`
|
|
4
|
+
entry file. The skill dispatches here from Step 5 after deciding whether
|
|
5
|
+
the project needs local-dev mocks.
|
|
6
|
+
|
|
7
|
+
## Runtime contexts
|
|
8
|
+
|
|
9
|
+
The goal is one compiled bundle that runs in either context:
|
|
10
|
+
|
|
11
|
+
| Context | Data source | Notes |
|
|
12
|
+
| ---------------------- | -------------------------------------------------------- | ------------------------------------------------------------- |
|
|
13
|
+
| **Local dev** | Mock `window.openai` shim in `bootstrap.js` | `npm run dev` only. No org, no ChatGPT host. |
|
|
14
|
+
| **ChatGPT / MCP host** | Host-provided `window.openai` (`callTool`, `toolOutput`) | Production. Same compiled bundle; mocks skipped by the guard. |
|
|
15
|
+
|
|
16
|
+
The guard `if (!window.openai?.callTool)` is what makes the bundle work
|
|
17
|
+
in both contexts: ChatGPT sets `window.openai` before loading the
|
|
18
|
+
bundle, so the guard short-circuits and real host calls run.
|
|
19
|
+
|
|
20
|
+
### ⚠ Mocks are for `npm run dev` only
|
|
21
|
+
|
|
22
|
+
Do **not** expect `npm run build && open dist/index.html` to render mock
|
|
23
|
+
data. Rollup hoists module-init code to the top of the bundle, which
|
|
24
|
+
means `@salesforce/sdk-chat`'s `detectSurface()` runs _before_ the
|
|
25
|
+
`main.js` / `bootstrap.js` body executes the `if (!window.openai?.
|
|
26
|
+
callTool)` block. The SDK caches surface = `"WebApp"` and never observes
|
|
27
|
+
the mock — LDS / graphql wire adapters silently return empty data.
|
|
28
|
+
|
|
29
|
+
- Dev server (`npm run dev`) works because Vite serves modules on demand
|
|
30
|
+
in source order; the entry file runs first.
|
|
31
|
+
- Production builds only target the real MCP host (where the host sets
|
|
32
|
+
`window.openai` before the bundle loads, so surface detection sees
|
|
33
|
+
`OpenAI` correctly).
|
|
34
|
+
|
|
35
|
+
When previewing production bundles locally (rare), drive the wrappers
|
|
36
|
+
by deploying the bundle to a real MCP server as a UI resource; the host
|
|
37
|
+
provides `window.openai` and wire adapters work there.
|
|
38
|
+
|
|
39
|
+
## Authoring pattern
|
|
40
|
+
|
|
41
|
+
Structure `bootstrap.js` as three ordered sections:
|
|
42
|
+
|
|
43
|
+
1. **Define mock data** at the top — hardcoded sample objects shaped like
|
|
44
|
+
the real tool output. This is the part the user edits per project.
|
|
45
|
+
2. **Install the shim** inside the `if (!window.openai?.callTool)`
|
|
46
|
+
guard. The shim returns responses shaped like the real MCP server's
|
|
47
|
+
responses.
|
|
48
|
+
3. **Dynamic-import LWC / SDK modules _after_ the shim** — SDK surface
|
|
49
|
+
detection runs at module load and reads `window.openai` once. Static
|
|
50
|
+
imports at the top of the file would fire the detection before the
|
|
51
|
+
shim is installed.
|
|
52
|
+
|
|
53
|
+
### Handling an existing `bootstrap.js`
|
|
54
|
+
|
|
55
|
+
If the project already has `bootstrap.js`, `main.js`, or another entry
|
|
56
|
+
file, do not overwrite it. Merge instead:
|
|
57
|
+
|
|
58
|
+
- **Static → dynamic imports:** If the existing file statically imports
|
|
59
|
+
`lwc` / LWC components, move those imports to dynamic `await
|
|
60
|
+
import(...)` calls after the shim install.
|
|
61
|
+
- **Existing `window.openai`:** If the file already installs a mock,
|
|
62
|
+
extend it rather than replace. Verify any guard already uses
|
|
63
|
+
`if (!window.openai?.callTool)` semantics.
|
|
64
|
+
- **Multiple roots:** If the existing file mounts more than one root
|
|
65
|
+
component, keep all of them; just make sure the shim install runs
|
|
66
|
+
before any dynamic import.
|
|
67
|
+
|
|
68
|
+
When in doubt, read the existing file top-to-bottom first, then ask the
|
|
69
|
+
user whether to edit or replace.
|
|
70
|
+
|
|
71
|
+
## Skeleton
|
|
72
|
+
|
|
73
|
+
Generate the following skeleton, adapting the mocked tool branches to
|
|
74
|
+
what was found in Step 4 of SKILL.md:
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
// 1) Mock data — shape after the real tool's sample output
|
|
78
|
+
const mockAccounts = [
|
|
79
|
+
{ id: "001a", name: "Acme", industry: "Technology", employees: 8500 },
|
|
80
|
+
{ id: "001b", name: "Global Media", industry: "Media", employees: 3200 },
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
// 2) Install the shim only when no host bridge exists.
|
|
84
|
+
// In ChatGPT / MCP context, the host already set window.openai, so
|
|
85
|
+
// this block is skipped and the real bridge is used.
|
|
86
|
+
if (!window.openai?.callTool) {
|
|
87
|
+
window.openai = {
|
|
88
|
+
...window.openai,
|
|
89
|
+
callTool: async (name, args) => {
|
|
90
|
+
// ── LDS (lightning/uiRecordApi.getRecord) ──────────────────────
|
|
91
|
+
if (name === "getRecordMcpTool") {
|
|
92
|
+
const a = mockAccounts.find((m) => m.id === args?.recordId);
|
|
93
|
+
if (!a) return { structuredContent: { data: null, error: "Record not found" } };
|
|
94
|
+
return {
|
|
95
|
+
structuredContent: {
|
|
96
|
+
data: {
|
|
97
|
+
fields: {
|
|
98
|
+
Name: { value: a.name },
|
|
99
|
+
Industry: { value: a.industry },
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── lightning/graphql (default tool name "graphqlQuery") ───────
|
|
107
|
+
if (name === "graphqlQuery") {
|
|
108
|
+
const query = args?.query ?? "";
|
|
109
|
+
if (query.includes("Account")) {
|
|
110
|
+
return {
|
|
111
|
+
result: JSON.stringify({
|
|
112
|
+
data: {
|
|
113
|
+
uiapi: {
|
|
114
|
+
query: {
|
|
115
|
+
Account: {
|
|
116
|
+
edges: mockAccounts.map((a) => ({
|
|
117
|
+
node: {
|
|
118
|
+
Id: a.id,
|
|
119
|
+
Name: { value: a.name },
|
|
120
|
+
Industry: { value: a.industry },
|
|
121
|
+
NumberOfEmployees: { value: a.employees },
|
|
122
|
+
},
|
|
123
|
+
})),
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
}),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
return { result: JSON.stringify({ data: {} }) };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
console.warn("[mock] unhandled tool:", name, args);
|
|
135
|
+
return { result: JSON.stringify({ data: {} }) };
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// For chat wrappers: seed toolOutput so the wrapper renders immediately
|
|
140
|
+
// in local dev instead of sitting in the "waiting" state.
|
|
141
|
+
window.openai.toolOutput = {
|
|
142
|
+
// Shape after the mapper's confirmed contract from Step 2d.
|
|
143
|
+
// e.g. { records: { "001a": { apiName: "Account", id: "001a", fields: { ... } } } }
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 3) Dynamic-import AFTER the shim so SDK surface detection sees the mock
|
|
148
|
+
const { createElement } = await import("lwc");
|
|
149
|
+
const { default: App } = await import("c/app");
|
|
150
|
+
document.getElementById("app").appendChild(createElement("c-app", { is: App }));
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## How response envelopes are unwrapped
|
|
154
|
+
|
|
155
|
+
The plugin's providers call `callTool()` and pass the result through
|
|
156
|
+
`normalizeMcpResponse()` before handing it to the wire adapter. That
|
|
157
|
+
helper accepts two shapes:
|
|
158
|
+
|
|
159
|
+
- `{ structuredContent: <payload> }` — MCP Apps surface shape. Returns
|
|
160
|
+
`<payload>` directly.
|
|
161
|
+
- `{ result: "<JSON string>" }` — OpenAI surface shape. Returns
|
|
162
|
+
`JSON.parse(<string>)`. (If the parsed value is an MCP-style content
|
|
163
|
+
array with a `text` block, it parses the inner JSON too.)
|
|
164
|
+
- Anything else — returned as-is.
|
|
165
|
+
|
|
166
|
+
**The wire adapter reads its tool-specific fields off the unwrapped
|
|
167
|
+
payload.** That payload must match the tool's contract:
|
|
168
|
+
|
|
169
|
+
- `getRecordMcpTool` (LDS wire adapter) reads `.data` and `.error` off
|
|
170
|
+
the unwrapped payload. Mock payload: `{ data: {...}, error?: ... }`.
|
|
171
|
+
- `graphqlQuery` (graphql wire adapter) reads `.data` and `.errors`
|
|
172
|
+
(plural, graphql-shaped) off the unwrapped payload. Mock payload:
|
|
173
|
+
`{ data: {...}, errors?: [...] }`.
|
|
174
|
+
|
|
175
|
+
Either envelope can carry either tool's payload. The tool-contract shape
|
|
176
|
+
is what matters. If the mock returns
|
|
177
|
+
`{ structuredContent: { data: {...} } }` for graphql, the wrapper reads
|
|
178
|
+
`.data` off `{ data: {...} }` which is **one level too shallow** — it
|
|
179
|
+
gets the real data. But if the mock instead nests another level, like
|
|
180
|
+
`{ structuredContent: { structuredContent: { data: {...} } } }` or
|
|
181
|
+
`{ structuredContent: { data: { data: {...} } } }`, the wire adapter
|
|
182
|
+
sees `data: { data: {...} }` and the wrapped component receives
|
|
183
|
+
`undefined` as graphql data.
|
|
184
|
+
|
|
185
|
+
Summary:
|
|
186
|
+
|
|
187
|
+
- **Don't re-wrap.** One envelope layer (`structuredContent` _or_
|
|
188
|
+
`result`), then the raw tool contract. No nested layers.
|
|
189
|
+
- **Mirror the real MCP server's response** when the mock differs from
|
|
190
|
+
what you see in production. Both envelopes work; pick whichever the
|
|
191
|
+
real server uses for the closest parity.
|
|
192
|
+
|
|
193
|
+
## Graphql query-based branching
|
|
194
|
+
|
|
195
|
+
Because the graphql wire adapter sends every query through a single tool
|
|
196
|
+
(`graphqlQuery` by default), the mock needs to branch on the query
|
|
197
|
+
string to return different data for different queries:
|
|
198
|
+
|
|
199
|
+
```js
|
|
200
|
+
if (name === "graphqlQuery") {
|
|
201
|
+
const query = args?.query ?? "";
|
|
202
|
+
if (query.includes("Account")) {
|
|
203
|
+
/* account edges */
|
|
204
|
+
}
|
|
205
|
+
if (query.includes("Contact")) {
|
|
206
|
+
/* contact edges */
|
|
207
|
+
}
|
|
208
|
+
if (query.includes("Opportunity")) {
|
|
209
|
+
/* opportunity edges */
|
|
210
|
+
}
|
|
211
|
+
return { result: JSON.stringify({ data: {} }) };
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
`args.variables` is also available if queries differ only by variables.
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# Chat Wrapper Flow
|
|
2
|
+
|
|
3
|
+
Walk through this reference whenever the user answers "Yes" to the Step 2
|
|
4
|
+
question in SKILL.md (any component needs to be driven by ChatGPT/MCP tool
|
|
5
|
+
output).
|
|
6
|
+
|
|
7
|
+
The chat-wrapper pattern inserts a thin adapter layer between the host's
|
|
8
|
+
`window.openai.toolOutput` and your LWC's `@api` props, so the component
|
|
9
|
+
stays unchanged and works in both standalone and host contexts:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
tool output ─▶ chatContextAdapter ─▶ <component>ChatMapper ─▶ <component>ChatWrapper ─▶ <your-core-component>
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Run the following sub-steps for each component the user wants to wrap.
|
|
16
|
+
Repeat for additional components if requested.
|
|
17
|
+
|
|
18
|
+
**Reference implementation:** the `recordDetail` example under
|
|
19
|
+
`examples/lwc-axl/lwc-records/sf/lwc/bcx/` in the webapps repo shows all
|
|
20
|
+
three bundles working together:
|
|
21
|
+
|
|
22
|
+
- `recordDetail/` — the unchanged core component.
|
|
23
|
+
- `recordDetailChatMapper/` — the full UI-API → `@api` mapper, including
|
|
24
|
+
`unwrapPropertiesMap`, envelope fallbacks, and GraphQL-edge support.
|
|
25
|
+
- `recordDetailChatWrapper/` — the wrapper with the initial-payload
|
|
26
|
+
debug log, loading/waiting states, and core-component mount.
|
|
27
|
+
- `chatContextAdapter/` — reusable across all wrappers in a project.
|
|
28
|
+
|
|
29
|
+
Copy patterns from those files when generating new bundles. The
|
|
30
|
+
instructions below describe the decision points; the reference code
|
|
31
|
+
fills in the exact implementation.
|
|
32
|
+
|
|
33
|
+
## 2a. Pick the target component
|
|
34
|
+
|
|
35
|
+
List the discovered components and ask:
|
|
36
|
+
|
|
37
|
+
> "Which component should get a chat wrapper? (Can repeat for more.)"
|
|
38
|
+
|
|
39
|
+
## 2b. Analyze the component's `@api` surface
|
|
40
|
+
|
|
41
|
+
Read the target component's `.js` file and extract the required prop
|
|
42
|
+
contract:
|
|
43
|
+
|
|
44
|
+
1. Collect every `@api <name>` declaration (and `@api get <name>()` pairs).
|
|
45
|
+
2. For each prop, note whether it's primitive, object, or array from its
|
|
46
|
+
usage (`wire({recordId: '$recordId', fields: '$fields'})` →
|
|
47
|
+
`recordId: string`, `fields: string[]`).
|
|
48
|
+
3. Note any `@wire` usage that consumes these props — those are the props
|
|
49
|
+
the mapper must populate.
|
|
50
|
+
|
|
51
|
+
Report what you found:
|
|
52
|
+
|
|
53
|
+
> "`bcx-record-detail` requires these `@api` props:
|
|
54
|
+
>
|
|
55
|
+
> - `recordId` (string) — identifies the record being displayed
|
|
56
|
+
> - `data` (object) — the record value (fields + apiName + id)
|
|
57
|
+
> - `fieldMetadata` (object) — per-field metadata from the org's objectInfos
|
|
58
|
+
> - `layout` (object) — per-record layout descriptor
|
|
59
|
+
> - `picklistValues` (object, optional)
|
|
60
|
+
> - `changedFields` (array, optional)
|
|
61
|
+
> - `expandable` (boolean, optional)"
|
|
62
|
+
|
|
63
|
+
## 2c. Ask about tool output structure
|
|
64
|
+
|
|
65
|
+
The mapper shape depends entirely on what the tool returns. Ask the user
|
|
66
|
+
for a sample payload:
|
|
67
|
+
|
|
68
|
+
> "What does the MCP tool output look like for this component? Paste a
|
|
69
|
+
> sample JSON payload, or give me the path to a sample file (e.g. an
|
|
70
|
+
> example file in the MCP server repo)."
|
|
71
|
+
|
|
72
|
+
Parse the payload and identify where each required prop lives. For
|
|
73
|
+
`getRecordDetails`-shaped output:
|
|
74
|
+
|
|
75
|
+
- `recordId` → `records[firstKey].id` (or the first key of the `records`
|
|
76
|
+
map)
|
|
77
|
+
- `fields` (qualified names like `"Account.Name"`) → derived from
|
|
78
|
+
`records[firstKey].apiName` + keys of `records[firstKey].fields`
|
|
79
|
+
- Full record data (for components that render the record directly) →
|
|
80
|
+
`records[firstKey]` (possibly after `normalizeFieldValue` unwrapping)
|
|
81
|
+
|
|
82
|
+
Accept common envelope variations: raw tool output, `structuredContent`
|
|
83
|
+
wrapper, nested `recordDetail`/`record_detail` keys.
|
|
84
|
+
|
|
85
|
+
### `.properties` unwrap for map-shaped fields
|
|
86
|
+
|
|
87
|
+
Some MCP hosts re-serialize `structuredContent` through an Avro /
|
|
88
|
+
JSON-schema view that wraps every map-typed field in a `.properties` key.
|
|
89
|
+
So instead of `records: { "001…": {...} }` the wrapper sees
|
|
90
|
+
`records: { properties: { "001…": {...} } }`. Without unwrapping, the
|
|
91
|
+
mapper's "first key" becomes the literal string `"properties"`.
|
|
92
|
+
|
|
93
|
+
**Rule:** every map-shaped field (`records`, `fields`, `objectInfos`,
|
|
94
|
+
`layouts`, …) must go through an `unwrapPropertiesMap(value)` helper that
|
|
95
|
+
returns `value.properties` when it looks like a properties-wrapped map,
|
|
96
|
+
else `value`. Apply this before reading keys or selecting the first
|
|
97
|
+
entry. The helper is small and always the same:
|
|
98
|
+
|
|
99
|
+
```js
|
|
100
|
+
function unwrapPropertiesMap(value) {
|
|
101
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
102
|
+
if (value.properties && typeof value.properties === "object") return value.properties;
|
|
103
|
+
return value;
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## 2d. Confirm the mapping with the user
|
|
108
|
+
|
|
109
|
+
Before generating files, show the planned mapping and ask for confirmation:
|
|
110
|
+
|
|
111
|
+
> "Here's how I'll map tool output → `bcx-record-detail` props:
|
|
112
|
+
>
|
|
113
|
+
> | Prop | Source in tool output |
|
|
114
|
+
> | -------------- | -------------------------------------------------------------- |
|
|
115
|
+
> | recordId | `records[firstKey].id` (falls back to first key) |
|
|
116
|
+
> | data | `records[firstKey]` (after `unwrapPropertiesMap` on `.fields`) |
|
|
117
|
+
> | fieldMetadata | `objectInfos[apiName]` |
|
|
118
|
+
> | layout | `layouts[apiName][recordTypeId].Full.View` |
|
|
119
|
+
> | picklistValues | derived from `objectInfos[apiName].fields[*].picklistValues` |
|
|
120
|
+
>
|
|
121
|
+
> Fallback: if the envelope wraps in `structuredContent` or
|
|
122
|
+
> `recordDetail`/`record_detail`, unwrap before reading.
|
|
123
|
+
>
|
|
124
|
+
> Does this mapping look correct? (yes / tell me what to change)"
|
|
125
|
+
|
|
126
|
+
Iterate until the user confirms. Do not proceed to file generation until
|
|
127
|
+
confirmation is explicit. Users frequently want to tweak field coverage or
|
|
128
|
+
add aliases at this step — taking the correction once saves a round of
|
|
129
|
+
re-generation.
|
|
130
|
+
|
|
131
|
+
## 2e. Generate the three bundles
|
|
132
|
+
|
|
133
|
+
Create three LWC bundles (adjust folder layout for the project type
|
|
134
|
+
detected in SKILL.md Step 1 — SFDX uses `force-app/main/default/lwc/`,
|
|
135
|
+
off-core uses `src/lwc/<namespace>/`):
|
|
136
|
+
|
|
137
|
+
### `chatContextAdapter/` — one per project
|
|
138
|
+
|
|
139
|
+
Reusable across every chat-wrapped component. Exports
|
|
140
|
+
`accessToolOutput()` and `subscribeToolOutput(listener)`. Reads
|
|
141
|
+
`window.openai.toolOutput`, subscribes to `openai:set_globals` and
|
|
142
|
+
`ui/notifications/tool-result` postMessages. If `chatContextAdapter`
|
|
143
|
+
already exists in the project, reuse it — don't regenerate.
|
|
144
|
+
|
|
145
|
+
Copy the reference implementation verbatim from
|
|
146
|
+
`docs/chat-wrapper-guide.md` in the plugin repo. It's stable and should
|
|
147
|
+
not be customized.
|
|
148
|
+
|
|
149
|
+
### `<component>ChatMapper/` — one per wrapped component
|
|
150
|
+
|
|
151
|
+
Exports `map<Component>ToolOutput(toolOutput)` that returns the confirmed
|
|
152
|
+
prop shape or `null` for invalid payloads. Keep it:
|
|
153
|
+
|
|
154
|
+
- **Pure and side-effect free** — no console logs, no DOM access.
|
|
155
|
+
- **Tolerant of envelope variations** — unwrap `structuredContent`,
|
|
156
|
+
`recordDetail`, `record_detail` where applicable.
|
|
157
|
+
- **Null-on-invalid** — never throw. The wrapper renders a waiting state
|
|
158
|
+
on `null`.
|
|
159
|
+
|
|
160
|
+
Always include the `unwrapPropertiesMap(value)` helper (see 2c) and call
|
|
161
|
+
it before reading any map-shaped field.
|
|
162
|
+
|
|
163
|
+
### `<component>ChatWrapper/` — one per wrapped component
|
|
164
|
+
|
|
165
|
+
On `connectedCallback`:
|
|
166
|
+
|
|
167
|
+
1. Call `subscribeToolOutput()` to listen for updates.
|
|
168
|
+
2. Call `accessToolOutput()` for the initial state.
|
|
169
|
+
3. Apply both through the mapper.
|
|
170
|
+
|
|
171
|
+
Renders:
|
|
172
|
+
|
|
173
|
+
- `loading` state while initializing
|
|
174
|
+
- `waiting` state when the mapper returns `null`
|
|
175
|
+
- the wrapped component with mapped props when mapping succeeds
|
|
176
|
+
|
|
177
|
+
**Important debugging aid:** Always log the raw initial tool output in
|
|
178
|
+
the wrapper's `connectedCallback`:
|
|
179
|
+
|
|
180
|
+
```js
|
|
181
|
+
const initial = accessToolOutput();
|
|
182
|
+
console.log(`[${wrapperName}] initial tool output`, initial);
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
The shape varies by host (Avro/`.properties` wrap, `structuredContent`
|
|
186
|
+
envelope, nested `recordDetail`, etc.). Seeing the real payload once tells
|
|
187
|
+
the user exactly which keys their mapper needs to unwrap. Leave this log
|
|
188
|
+
in by default — removing it makes iterative debugging much harder for
|
|
189
|
+
little benefit.
|
|
190
|
+
|
|
191
|
+
Each bundle needs the matching `.js-meta.xml` (SFDX) with
|
|
192
|
+
`<isExposed>false</isExposed>` unless the user needs it exposed.
|
|
193
|
+
|
|
194
|
+
### Announce the wrapper tag name
|
|
195
|
+
|
|
196
|
+
Mention the wrapper tag name so it can be referenced in Step 3 if the
|
|
197
|
+
user wants the wrapper itself (or a parent embedding it) as the root:
|
|
198
|
+
|
|
199
|
+
> "Created `bcx-record-detail-chat-wrapper`. You can drop
|
|
200
|
+
> `<bcx-record-detail-chat-wrapper>` into any parent (e.g. your root app
|
|
201
|
+
> component), or pick it directly as the root in the next step."
|
|
202
|
+
|
|
203
|
+
## Debugging the mapping
|
|
204
|
+
|
|
205
|
+
If the wrapper stays in the "Waiting for tool output..." state even
|
|
206
|
+
though the tool fires, **the initial-payload log is the single most
|
|
207
|
+
useful debugging tool.** Check the DevTools console (or the MCP Jam
|
|
208
|
+
widget log) for the `[…ChatWrapper] initial tool output` line and compare
|
|
209
|
+
the actual shape to what the mapper expects.
|
|
210
|
+
|
|
211
|
+
Common reasons the mapper returns `null`:
|
|
212
|
+
|
|
213
|
+
- **`.properties` wrap:** `records` arrives as
|
|
214
|
+
`{ properties: { "001…": {...} } }` instead of `{ "001…": {...} }`.
|
|
215
|
+
Fix: add/verify `unwrapPropertiesMap` on every map-shaped field.
|
|
216
|
+
- **Envelope mismatch:** payload is wrapped in `structuredContent`,
|
|
217
|
+
`recordDetail`, or `record_detail` and the mapper reads from the top
|
|
218
|
+
level. Fix: add the missing unwrap branch in `unwrapEnvelope`.
|
|
219
|
+
- **Key-alias mismatch:** camelCase vs snake_case (`recordId` vs
|
|
220
|
+
`record_id`, `objectInfos` vs `object_infos`).
|
|
221
|
+
- **Empty fields map:** the tool returned a record with no `fields`,
|
|
222
|
+
usually because the tool's `outputSchema` doesn't project the fields
|
|
223
|
+
the component needs. Fix the tool, not the mapper.
|
|
224
|
+
|
|
225
|
+
Once the real shape is visible, update the mapper's unwraps to match,
|
|
226
|
+
and confirm the mapper returns a non-null object by checking that the
|
|
227
|
+
`[…ChatWrapper] mapper returned null` warning disappears.
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# Known Pitfalls
|
|
2
|
+
|
|
3
|
+
Consolidated list of bugs and confusions that have surfaced in real
|
|
4
|
+
consumer projects. Read this when setting up a new project, and consult
|
|
5
|
+
it when debugging a project that's already set up but misbehaving.
|
|
6
|
+
|
|
7
|
+
Each entry has a **Symptom**, **Cause**, and **Fix** so the skill can
|
|
8
|
+
recognize an issue from console output alone.
|
|
9
|
+
|
|
10
|
+
## 1. Node version mismatch (Vite 8 + rolldown)
|
|
11
|
+
|
|
12
|
+
**Symptom:** `vite build` crashes inside `rolldown` with "Cannot find
|
|
13
|
+
native binding. npm has a bug related to optional dependencies."
|
|
14
|
+
|
|
15
|
+
**Cause:** Vite 8.x requires Node `^20.19.0 || >=22.12.0`. On older
|
|
16
|
+
Node 20.x (e.g. 20.14) the rolldown optional dependency for the
|
|
17
|
+
platform binary doesn't install, producing an opaque error instead of a
|
|
18
|
+
clear engine mismatch.
|
|
19
|
+
|
|
20
|
+
**Fix:** Either upgrade Node (`nvm install 20.19` or `22`), or pin
|
|
21
|
+
`vite@^7` and a compatible `@lwc/rollup-plugin` major.
|
|
22
|
+
|
|
23
|
+
## 2. Hardcoded dep versions (prerelease / alpha unpublished)
|
|
24
|
+
|
|
25
|
+
**Symptom:** `npm install` fails with
|
|
26
|
+
`No matching version found for <pkg>@^1.28.17-alpha` or similar.
|
|
27
|
+
|
|
28
|
+
**Cause:** The consumer guide's package.json snippet includes example
|
|
29
|
+
version pins (like `^1.28.17-alpha` for `lightning-base-components`).
|
|
30
|
+
Prerelease tags get unpublished from the registry over time.
|
|
31
|
+
|
|
32
|
+
**Fix:** Look up the current published version with
|
|
33
|
+
`npm view <pkg> version` and pin to `^<that>` instead of copying from
|
|
34
|
+
the snippet.
|
|
35
|
+
|
|
36
|
+
## 3. Hand-rolled `lightningGraphql` in `vite.config.js`
|
|
37
|
+
|
|
38
|
+
**Symptom:** `@wire(graphql)` returns empty data; console shows
|
|
39
|
+
`No window.openai.callTool available` or a hand-written
|
|
40
|
+
`[lightningGraphql] ...` log message.
|
|
41
|
+
|
|
42
|
+
**Cause:** The generator treated the "include `builtins.lightningGraphql()`"
|
|
43
|
+
instruction as "write a provider called `lightningGraphql`" and inlined
|
|
44
|
+
a custom provider factory in `vite.config.js` instead of importing
|
|
45
|
+
`builtins` from the plugin. The hand-rolled version bypasses the
|
|
46
|
+
plugin's real runtime (which handles `globalThis.__sfdc_sdk__`,
|
|
47
|
+
`getChatSDK()`, and `normalizeMcpResponse`).
|
|
48
|
+
|
|
49
|
+
**Fix:** Replace with:
|
|
50
|
+
|
|
51
|
+
```js
|
|
52
|
+
import lwcVitePlugin, { builtins } from "@salesforce/vite-plugin-lwc-ui-bundle";
|
|
53
|
+
// inside lwcVitePlugin({ providers: [...] })
|
|
54
|
+
builtins.lightningGraphql(),
|
|
55
|
+
builtins.lds(),
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Same rule for `lds` and every other provider — never hand-write them.
|
|
59
|
+
|
|
60
|
+
## 4. `this`-binding bug in `lightning/graphql` runtime
|
|
61
|
+
|
|
62
|
+
**Symptom:** `@wire(graphql)` with a class-based SDK (e.g.
|
|
63
|
+
`@salesforce/sdk-data`'s `WebApp`) silently emits
|
|
64
|
+
`{data: undefined, errors: [TypeError]}`; component falls through to an
|
|
65
|
+
empty state.
|
|
66
|
+
|
|
67
|
+
**Cause:** Plugin versions ≤ 1.132.0 destructure `graphql` off
|
|
68
|
+
`globalThis.__sfdc_sdk__` before calling it, which loses the method's
|
|
69
|
+
`this` binding. The method internally calls `this.fetch(...)` and
|
|
70
|
+
blows up.
|
|
71
|
+
|
|
72
|
+
**Fix:** Fixed in webapps PR #477 (plugin version > 1.132.0). Until the
|
|
73
|
+
fix ships in a release, add this to `bootstrap.js` after creating the
|
|
74
|
+
SDK:
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
sdk.graphql = sdk.graphql.bind(sdk);
|
|
78
|
+
sdk.fetch = sdk.fetch.bind(sdk);
|
|
79
|
+
globalThis.__sfdc_sdk__ = sdk;
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## 5. Wrong tool name in mock branch (graphql foot-gun)
|
|
83
|
+
|
|
84
|
+
**Symptom:** `@wire(graphql)` returns empty data even though the mock
|
|
85
|
+
shim is installed; console shows "[mock] unhandled tool: graphqlQuery"
|
|
86
|
+
or the graphql mock branch is never entered.
|
|
87
|
+
|
|
88
|
+
**Cause:** The wire adapter calls `callTool("graphqlQuery", { query,
|
|
89
|
+
variables })` by default, but the mock branches on `"graphql"` or
|
|
90
|
+
`"lightningGraphql"`.
|
|
91
|
+
|
|
92
|
+
**Fix:** Name the branch `"graphqlQuery"` (literal default) — or pass
|
|
93
|
+
the same custom name to both the provider and the mock:
|
|
94
|
+
|
|
95
|
+
```js
|
|
96
|
+
// vite.config.js
|
|
97
|
+
builtins.lightningGraphql({ toolName: "myCustomGraphql" }),
|
|
98
|
+
|
|
99
|
+
// bootstrap.js
|
|
100
|
+
if (name === "myCustomGraphql") { /* ... */ }
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The vite-config tool name and the mock-branch string must match
|
|
104
|
+
exactly.
|
|
105
|
+
|
|
106
|
+
## 6. `.properties` wrap in Avro/JSON-schema-serialized tool output
|
|
107
|
+
|
|
108
|
+
**Symptom:** Chat wrapper renders "Waiting for tool output..." even
|
|
109
|
+
though the host fired tool output; mapper's `Object.keys(records)[0]`
|
|
110
|
+
returns the literal string `"properties"`.
|
|
111
|
+
|
|
112
|
+
**Cause:** Some MCP hosts re-serialize `structuredContent` through an
|
|
113
|
+
Avro / JSON-schema view that wraps every map-typed field in a
|
|
114
|
+
`.properties` key. So `records: { "001…": {...} }` becomes
|
|
115
|
+
`records: { properties: { "001…": {...} } }`.
|
|
116
|
+
|
|
117
|
+
**Fix:** Every map-shaped field (`records`, `fields`, `objectInfos`,
|
|
118
|
+
`layouts`, …) must go through an `unwrapPropertiesMap(value)` helper
|
|
119
|
+
before reading keys:
|
|
120
|
+
|
|
121
|
+
```js
|
|
122
|
+
function unwrapPropertiesMap(value) {
|
|
123
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
124
|
+
if (value.properties && typeof value.properties === "object") return value.properties;
|
|
125
|
+
return value;
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Apply this in every mapper that reads map-shaped fields.
|
|
130
|
+
|
|
131
|
+
## 7. Missing `viteSingleFile()`
|
|
132
|
+
|
|
133
|
+
**Symptom:** `dist/index.html` is 1-2 KB (just a stub); component
|
|
134
|
+
doesn't render when opened locally.
|
|
135
|
+
|
|
136
|
+
**Cause:** `viteSingleFile()` is not in the Vite plugins array, so
|
|
137
|
+
Vite emits its default multi-file output (HTML stub + separate
|
|
138
|
+
`assets/*.js` + `assets/*.css`).
|
|
139
|
+
|
|
140
|
+
**Fix:** Add to `vite.config.js`:
|
|
141
|
+
|
|
142
|
+
```js
|
|
143
|
+
import { viteSingleFile } from "vite-plugin-singlefile";
|
|
144
|
+
// ...
|
|
145
|
+
plugins: [
|
|
146
|
+
lwcVitePlugin({ /* ... */ }),
|
|
147
|
+
viteSingleFile(),
|
|
148
|
+
],
|
|
149
|
+
build: {
|
|
150
|
+
target: "esnext",
|
|
151
|
+
cssCodeSplit: false,
|
|
152
|
+
rollupOptions: { output: { inlineDynamicImports: true } },
|
|
153
|
+
},
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
A correctly built `dist/index.html` is typically 100 KB+.
|
|
157
|
+
|
|
158
|
+
## 8. `@salesforce/label/...` warnings
|
|
159
|
+
|
|
160
|
+
**Symptom:** Build warns `Unhandled import: @salesforce/label/...`,
|
|
161
|
+
but the build succeeds.
|
|
162
|
+
|
|
163
|
+
**Cause:** Expected. The plugin returns the label key as display text
|
|
164
|
+
when no override is provided.
|
|
165
|
+
|
|
166
|
+
**Fix:** Not a bug. Add overrides to `builtins.label({...})` to
|
|
167
|
+
customize the displayed text.
|
|
168
|
+
|
|
169
|
+
## 9. Missing gate / accessCheck providers with lightning-base-components
|
|
170
|
+
|
|
171
|
+
**Symptom:** Build fails with `Cannot read properties of undefined
|
|
172
|
+
(reading 'isOpen')`.
|
|
173
|
+
|
|
174
|
+
**Cause:** `lightning-base-components` use `@salesforce/gate/*` and
|
|
175
|
+
`@salesforce/accessCheck/*` internally, even if user code doesn't
|
|
176
|
+
import them.
|
|
177
|
+
|
|
178
|
+
**Fix:** Always include `builtins.gate()` and `builtins.accessCheck()`
|
|
179
|
+
(and `builtins.primitiveUtils()`) whenever `lightning-base-components`
|
|
180
|
+
is in the modules config.
|
|
181
|
+
|
|
182
|
+
## 10. Core-only imports
|
|
183
|
+
|
|
184
|
+
**Symptom:** `Rollup failed to resolve import "force/someModule"` (or
|
|
185
|
+
`aura`, `logger`, etc.).
|
|
186
|
+
|
|
187
|
+
**Cause:** Platform-only modules that don't exist off-platform.
|
|
188
|
+
|
|
189
|
+
**Fix:** Provide a stub via the plugin's `stubs` option:
|
|
190
|
+
|
|
191
|
+
```js
|
|
192
|
+
lwcVitePlugin({
|
|
193
|
+
// ...
|
|
194
|
+
stubs: { "force/someModule": "./stubs/someModule.js" },
|
|
195
|
+
});
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Point the stub at a minimal file that exports the shape the importer
|
|
199
|
+
expects.
|
|
200
|
+
|
|
201
|
+
## 11. Mock data doesn't work in production build
|
|
202
|
+
|
|
203
|
+
**Symptom:** `npm run build && open dist/index.html` shows wrappers in
|
|
204
|
+
"Waiting" state; `sdk.callTool is not available on this surface` in
|
|
205
|
+
the console. Dev server (`npm run dev`) works fine.
|
|
206
|
+
|
|
207
|
+
**Cause:** Rollup hoists module-init code to the top of the bundle.
|
|
208
|
+
`@salesforce/sdk-chat`'s top-level `const surface = detectSurface()`
|
|
209
|
+
runs before `main.js` / `bootstrap.js` installs the mock, so the SDK
|
|
210
|
+
caches `surface = "WebApp"` forever.
|
|
211
|
+
|
|
212
|
+
**Fix:** Not supported — mocks are for `npm run dev` only. Production
|
|
213
|
+
builds only target real MCP hosts (where the host sets `window.openai`
|
|
214
|
+
before the bundle loads, so surface detection correctly resolves to
|
|
215
|
+
`OpenAI`). See `bootstrap-js-patterns.md` for the full explanation.
|