@salesforce/webapp-experimental 1.72.0 → 1.73.1

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.
@@ -29,9 +29,15 @@ export type ProxyHandler = (req: IncomingMessage, res: ServerResponse, next?: ()
29
29
  *
30
30
  * @param manifest - WebApp manifest configuration
31
31
  * @param orgInfo - Salesforce org information
32
- * @param target - Target URL for dev server forwarding
32
+ * @param target - Target URL for dev server forwarding (optional)
33
+ * @param basePath - Base path for routing (optional)
33
34
  * @param options - Proxy configuration options
34
35
  * @returns Async request handler function for Node.js HTTP server
35
36
  */
36
37
  export declare function createProxyHandler(manifest: WebAppManifest, orgInfo?: OrgInfo, target?: string, basePath?: string, options?: ProxyOptions): ProxyHandler;
38
+ /**
39
+ * Inject Live Preview script into HTML content.
40
+ * Used by the Vite plugin's transformIndexHtml hook and the standalone proxy's sendResponse.
41
+ */
42
+ export declare function injectLivePreviewScript(html: string): string;
37
43
  //# sourceMappingURL=handler.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/proxy/handler.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAGjE,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAM/D;;GAEG;AACH,eAAO,MAAM,yBAAyB,uBAAuB,CAAC;AAE9D;;GAEG;AACH,eAAO,MAAM,mBAAmB,8BAA8B,CAAC;AAO/D;;GAEG;AACH,MAAM,WAAW,YAAY;IAE5B,KAAK,CAAC,EAAE,OAAO,CAAC;IAEhB,cAAc,CAAC,EAAE,CAAC,cAAc,EAAE,OAAO,KAAK,IAAI,CAAC;CACnD;AAED;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG,CAC1B,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,cAAc,EACnB,IAAI,CAAC,EAAE,MAAM,IAAI,KACb,OAAO,CAAC,IAAI,CAAC,CAAC;AAofnB;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CACjC,QAAQ,EAAE,cAAc,EACxB,OAAO,CAAC,EAAE,OAAO,EACjB,MAAM,CAAC,EAAE,MAAM,EACf,QAAQ,CAAC,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE,YAAY,GACpB,YAAY,CAGd"}
1
+ {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/proxy/handler.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAIjE,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAM/D;;GAEG;AACH,eAAO,MAAM,yBAAyB,uBAAuB,CAAC;AAE9D;;GAEG;AACH,eAAO,MAAM,mBAAmB,8BAA8B,CAAC;AA2B/D;;GAEG;AACH,MAAM,WAAW,YAAY;IAE5B,KAAK,CAAC,EAAE,OAAO,CAAC;IAEhB,cAAc,CAAC,EAAE,CAAC,cAAc,EAAE,OAAO,KAAK,IAAI,CAAC;CACnD;AAED;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG,CAC1B,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,cAAc,EACnB,IAAI,CAAC,EAAE,MAAM,IAAI,KACb,OAAO,CAAC,IAAI,CAAC,CAAC;AAmkBnB;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CACjC,QAAQ,EAAE,cAAc,EACxB,OAAO,CAAC,EAAE,OAAO,EACjB,MAAM,CAAC,EAAE,MAAM,EACf,QAAQ,CAAC,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE,YAAY,GACpB,YAAY,CAGd;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE5D"}
@@ -4,6 +4,7 @@
4
4
  * For full license text, see the LICENSE.txt file
5
5
  */
6
6
  import { createRequire } from "node:module";
7
+ import { getLivePreviewScriptContent, LIVE_PREVIEW_SCRIPT_MARKER } from "./livePreviewScript.js";
7
8
  import { applyTrailingSlash, matchRoute } from "./routing.js";
8
9
  import { refreshOrgAuth } from "../app/index.js";
9
10
  const require = createRequire(import.meta.url);
@@ -20,6 +21,23 @@ export const WEBAPP_PROXY_HEADER = "X-Salesforce-WebApp-Proxy";
20
21
  * Endpoint for Lightning Out frontdoor URL generation
21
22
  */
22
23
  const LIGHTNING_OUT_SINGLE_ACCESS_PATH = "/services/oauth2/singleaccess";
24
+ const SALESFORCE_API_PREFIXES = ["/services/", "/lwr/apex/"];
25
+ const AUTH_FAILED_RESPONSE = {
26
+ error: "AUTHENTICATION_FAILED",
27
+ message: "Authentication failed. Please re-authenticate to your Salesforce org.",
28
+ status: 401,
29
+ };
30
+ /**
31
+ * Thrown when a Salesforce authentication operation fails
32
+ * (e.g. token refresh failure, missing credentials).
33
+ * Caught in request handlers to return a 401 response.
34
+ */
35
+ class AuthenticationError extends Error {
36
+ constructor(message) {
37
+ super(message);
38
+ this.name = "AuthenticationError";
39
+ }
40
+ }
23
41
  /**
24
42
  * Handles all proxy routing and forwarding for WebApps
25
43
  */
@@ -73,6 +91,13 @@ class WebAppProxyHandler {
73
91
  if (this.options?.debug) {
74
92
  console.log(`[webapps-proxy] Rewrite to ${req.url}`);
75
93
  }
94
+ // After rewrite, let Vite handle the rewritten request (or forward to dev server if no next)
95
+ if (next) {
96
+ next();
97
+ return;
98
+ }
99
+ await this.forwardToDevServer(req, res);
100
+ return;
76
101
  }
77
102
  if (match.type === "file-upload") {
78
103
  console.log("[webapps-proxy] file-upload match found → handleFileUpload");
@@ -80,6 +105,14 @@ class WebAppProxyHandler {
80
105
  return;
81
106
  }
82
107
  }
108
+ // Check if this is a Salesforce API request that needs proxying
109
+ if (SALESFORCE_API_PREFIXES.some((prefix) => pathname.startsWith(prefix))) {
110
+ await this.handleSalesforceApi(req, res);
111
+ return;
112
+ }
113
+ // For all other requests (static assets, HMR, HTML), let Vite handle them directly
114
+ // Note: In Vite middleware mode, HTML script injection is handled by
115
+ // the Vite plugin's transformIndexHtml hook, not here.
83
116
  if (next) {
84
117
  next();
85
118
  }
@@ -87,6 +120,26 @@ class WebAppProxyHandler {
87
120
  await this.forwardToDevServer(req, res);
88
121
  }
89
122
  }
123
+ async forwardToDevServer(req, res) {
124
+ try {
125
+ const baseUrl = this.target ?? `http://${req.headers.host ?? "localhost"}`;
126
+ const url = new URL(req.url ?? "/", baseUrl);
127
+ if (this.options?.debug) {
128
+ console.log(`[webapps-proxy] Forwarding to dev server: ${url.href}`);
129
+ }
130
+ const body = req.method !== "GET" && req.method !== "HEAD" ? await getBody(req) : undefined;
131
+ const response = await fetch(url.href, {
132
+ method: req.method,
133
+ headers: getFilteredHeaders(req.headers),
134
+ body: body,
135
+ });
136
+ await this.sendResponse(res, response);
137
+ }
138
+ catch (error) {
139
+ console.error("[webapps-proxy] Dev server request failed:", error);
140
+ this.sendAuthOrGatewayError(res, error, "Failed to forward request to dev server");
141
+ }
142
+ }
90
143
  handleRedirect(res, location, statusCode) {
91
144
  res.writeHead(statusCode, { Location: location });
92
145
  res.end();
@@ -121,6 +174,17 @@ class WebAppProxyHandler {
121
174
  res.writeHead(statusCode, { "Content-Type": "application/json" });
122
175
  res.end(JSON.stringify(body));
123
176
  }
177
+ sendAuthOrGatewayError(res, error, gatewayMessage) {
178
+ if (error instanceof AuthenticationError) {
179
+ this.sendJson(res, 401, AUTH_FAILED_RESPONSE);
180
+ }
181
+ else {
182
+ this.sendJson(res, 502, {
183
+ error: "GATEWAY_ERROR",
184
+ message: gatewayMessage,
185
+ });
186
+ }
187
+ }
124
188
  async handleLightningOutFrontdoor(req, res, _url) {
125
189
  try {
126
190
  if (!this.orgInfo) {
@@ -166,7 +230,7 @@ class WebAppProxyHandler {
166
230
  if (response.status === 401 || response.status === 403) {
167
231
  const updatedOrgInfo = await this.refreshToken();
168
232
  if (!updatedOrgInfo) {
169
- throw new Error("Failed to refresh token");
233
+ throw new AuthenticationError("Failed to refresh token");
170
234
  }
171
235
  baseUrl = updatedOrgInfo.rawInstanceUrl.replace(/\/$/, "");
172
236
  response = await fetch(`${baseUrl}${LIGHTNING_OUT_SINGLE_ACCESS_PATH}`, {
@@ -219,7 +283,9 @@ class WebAppProxyHandler {
219
283
  console.warn(`[webapps-proxy] Received ${response.status}, refreshing token...`);
220
284
  const updatedOrgInfo = await this.refreshToken();
221
285
  if (updatedOrgInfo === undefined) {
222
- throw new Error("Failed to refresh token");
286
+ console.error("[webapps-proxy] Failed to refresh token - authentication error");
287
+ this.sendJson(res, 401, AUTH_FAILED_RESPONSE);
288
+ return;
223
289
  }
224
290
  if (this.options?.debug) {
225
291
  console.log("[webapps-proxy] Token refreshed, retrying request");
@@ -241,11 +307,7 @@ class WebAppProxyHandler {
241
307
  }
242
308
  catch (error) {
243
309
  console.error("[webapps-proxy] Salesforce API request failed:", error);
244
- res.writeHead(502, { "Content-Type": "application/json" });
245
- res.end(JSON.stringify({
246
- error: "GATEWAY_ERROR",
247
- message: "Failed to forward request to Salesforce",
248
- }));
310
+ this.sendAuthOrGatewayError(res, error, "Failed to forward request to Salesforce");
249
311
  }
250
312
  }
251
313
  async refreshToken() {
@@ -256,7 +318,7 @@ class WebAppProxyHandler {
256
318
  // This handles cases where orgInfo was created without an explicit alias
257
319
  const refreshIdentifier = this.orgInfo.orgAlias || this.orgInfo.username;
258
320
  if (!refreshIdentifier) {
259
- throw new Error("Cannot refresh token: no org alias or username available");
321
+ throw new AuthenticationError("Cannot refresh token: no org alias or username available");
260
322
  }
261
323
  const updatedOrgInfo = await refreshOrgAuth(refreshIdentifier);
262
324
  if (!updatedOrgInfo) {
@@ -310,39 +372,17 @@ class WebAppProxyHandler {
310
372
  body: body,
311
373
  });
312
374
  }
375
+ else {
376
+ console.error("[webapps-proxy] Failed to refresh token for GraphQL - authentication error");
377
+ this.sendJson(res, 401, AUTH_FAILED_RESPONSE);
378
+ return;
379
+ }
313
380
  }
314
381
  await this.sendResponse(res, response);
315
382
  }
316
383
  catch (error) {
317
384
  console.error("[webapps-proxy] GraphQL request failed:", error);
318
- res.writeHead(502, { "Content-Type": "application/json" });
319
- res.end(JSON.stringify({
320
- error: "GATEWAY_ERROR",
321
- message: "Failed to forward GraphQL request to Salesforce",
322
- }));
323
- }
324
- }
325
- async forwardToDevServer(req, res) {
326
- try {
327
- const url = new URL(req.url ?? "/", this.target);
328
- if (this.options?.debug) {
329
- console.log(`[webapps-proxy] Forwarding to dev server: ${url.href}`);
330
- }
331
- const body = req.method !== "GET" && req.method !== "HEAD" ? await getBody(req) : undefined;
332
- const response = await fetch(url.href, {
333
- method: req.method,
334
- headers: getFilteredHeaders(req.headers),
335
- body: body,
336
- });
337
- await this.sendResponse(res, response);
338
- }
339
- catch (error) {
340
- console.error("[webapps-proxy] Dev server request failed:", error);
341
- res.writeHead(502, { "Content-Type": "application/json" });
342
- res.end(JSON.stringify({
343
- error: "GATEWAY_ERROR",
344
- message: "Failed to forward request to dev server",
345
- }));
385
+ this.sendAuthOrGatewayError(res, error, "Failed to forward GraphQL request to Salesforce");
346
386
  }
347
387
  }
348
388
  async sendResponse(res, response) {
@@ -353,18 +393,67 @@ class WebAppProxyHandler {
353
393
  headers[key] = value;
354
394
  }
355
395
  });
356
- res.writeHead(response.status, headers);
357
- if (response.body) {
396
+ // Check if response is HTML and should have script injected
397
+ const contentType = response.headers.get("content-type") || "";
398
+ const isHtml = contentType.includes("text/html");
399
+ if (isHtml && response.body) {
400
+ // Buffer the entire response body to inject script
401
+ const chunks = [];
358
402
  const reader = response.body.getReader();
359
403
  while (true) {
360
404
  const { done, value } = await reader.read();
361
405
  if (done)
362
406
  break;
363
- res.write(value);
407
+ chunks.push(value);
408
+ }
409
+ // Combine all chunks into a single buffer
410
+ const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
411
+ const buffer = new Uint8Array(totalLength);
412
+ let offset = 0;
413
+ for (const chunk of chunks) {
414
+ buffer.set(chunk, offset);
415
+ offset += chunk.length;
416
+ }
417
+ // Convert to string, inject script, and convert back
418
+ const html = new TextDecoder().decode(buffer);
419
+ const modifiedHtml = WebAppProxyHandler.injectLivePreviewScript(html);
420
+ // Update content-length header
421
+ const modifiedBuffer = new TextEncoder().encode(modifiedHtml);
422
+ headers["content-length"] = modifiedBuffer.length.toString();
423
+ res.writeHead(response.status, headers);
424
+ res.write(modifiedBuffer);
425
+ }
426
+ else {
427
+ // For non-HTML responses, stream as before
428
+ res.writeHead(response.status, headers);
429
+ if (response.body) {
430
+ const reader = response.body.getReader();
431
+ while (true) {
432
+ const { done, value } = await reader.read();
433
+ if (done)
434
+ break;
435
+ res.write(value);
436
+ }
364
437
  }
365
438
  }
366
439
  res.end();
367
440
  }
441
+ static injectLivePreviewScript(html) {
442
+ if (html.includes(LIVE_PREVIEW_SCRIPT_MARKER)) {
443
+ return html;
444
+ }
445
+ const scriptContent = getLivePreviewScriptContent();
446
+ const scriptTag = `<script ${LIVE_PREVIEW_SCRIPT_MARKER}>` + scriptContent + "<" + "/script>";
447
+ if (html.includes("</body>")) {
448
+ const lastBodyIndex = html.lastIndexOf("</body>");
449
+ return html.substring(0, lastBodyIndex) + scriptTag + html.substring(lastBodyIndex);
450
+ }
451
+ if (html.includes("</html>")) {
452
+ const lastHtmlIndex = html.lastIndexOf("</html>");
453
+ return html.substring(0, lastHtmlIndex) + scriptTag + html.substring(lastHtmlIndex);
454
+ }
455
+ return html + scriptTag;
456
+ }
368
457
  /**
369
458
  * Proxy POST /chatter/handlers/file/body (XHR/file upload) to Salesforce.
370
459
  * Uses rawInstanceUrl for Chatter API. Preserves multipart/form-data from XHR.
@@ -426,7 +515,8 @@ class WebAppProxyHandler {
426
515
  *
427
516
  * @param manifest - WebApp manifest configuration
428
517
  * @param orgInfo - Salesforce org information
429
- * @param target - Target URL for dev server forwarding
518
+ * @param target - Target URL for dev server forwarding (optional)
519
+ * @param basePath - Base path for routing (optional)
430
520
  * @param options - Proxy configuration options
431
521
  * @returns Async request handler function for Node.js HTTP server
432
522
  */
@@ -434,6 +524,13 @@ export function createProxyHandler(manifest, orgInfo, target, basePath, options)
434
524
  const handler = new WebAppProxyHandler(manifest, orgInfo, target, basePath, options);
435
525
  return (req, res, next) => handler.handle(req, res, next);
436
526
  }
527
+ /**
528
+ * Inject Live Preview script into HTML content.
529
+ * Used by the Vite plugin's transformIndexHtml hook and the standalone proxy's sendResponse.
530
+ */
531
+ export function injectLivePreviewScript(html) {
532
+ return WebAppProxyHandler.injectLivePreviewScript(html);
533
+ }
437
534
  function getFilteredHeaders(headers) {
438
535
  const filtered = {};
439
536
  const hopByHopHeaders = new Set([
@@ -448,7 +545,11 @@ function getFilteredHeaders(headers) {
448
545
  ]);
449
546
  for (const [key, value] of Object.entries(headers)) {
450
547
  if (!hopByHopHeaders.has(key.toLowerCase()) && value) {
451
- filtered[key] = Array.isArray(value) ? value.join(", ") : value;
548
+ filtered[key] = Array.isArray(value)
549
+ ? value.join(", ")
550
+ : typeof value === "string"
551
+ ? value
552
+ : String(value);
452
553
  }
453
554
  }
454
555
  return filtered;
@@ -4,6 +4,6 @@
4
4
  * For full license text, see the LICENSE.txt file
5
5
  */
6
6
  export type { ProxyOptions, ProxyHandler } from "./handler.js";
7
- export { createProxyHandler, WEBAPP_HEALTH_CHECK_PARAM, WEBAPP_PROXY_HEADER } from "./handler.js";
7
+ export { createProxyHandler, injectLivePreviewScript, WEBAPP_HEALTH_CHECK_PARAM, WEBAPP_PROXY_HEADER, } from "./handler.js";
8
8
  export { getErrorPageTemplate } from "./error-page.js";
9
9
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/proxy/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,yBAAyB,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAClG,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/proxy/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC/D,OAAO,EACN,kBAAkB,EAClB,uBAAuB,EACvB,yBAAyB,EACzB,mBAAmB,GACnB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC"}
@@ -3,5 +3,5 @@
3
3
  * All rights reserved.
4
4
  * For full license text, see the LICENSE.txt file
5
5
  */
6
- export { createProxyHandler, WEBAPP_HEALTH_CHECK_PARAM, WEBAPP_PROXY_HEADER } from "./handler.js";
6
+ export { createProxyHandler, injectLivePreviewScript, WEBAPP_HEALTH_CHECK_PARAM, WEBAPP_PROXY_HEADER, } from "./handler.js";
7
7
  export { getErrorPageTemplate } from "./error-page.js";
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Copyright (c) 2026, Salesforce, Inc.,
3
+ * All rights reserved.
4
+ * For full license text, see the LICENSE.txt file
5
+ */
6
+ /**
7
+ * Returns the JavaScript source that gets injected into previewed web apps.
8
+ * Reads from templates/livePreviewScript.js at runtime (copied to dist/ by postbuild).
9
+ * Cached after first read.
10
+ *
11
+ * Responsibilities:
12
+ * - Fetch interceptor for network-error detection (runs synchronously on load)
13
+ * - Runtime / compile / HMR error listeners
14
+ * - Error deduplication
15
+ * - postMessage bridge to the VS Code webview (when running inside an iframe)
16
+ * - Standalone browser error overlay (when NOT inside an iframe)
17
+ * - Copy / paste / right-click bridge for VS Code webview
18
+ */
19
+ export declare function getLivePreviewScriptContent(): string;
20
+ /** Data attribute used to detect (and prevent) double injection. */
21
+ export declare const LIVE_PREVIEW_SCRIPT_MARKER = "data-live-preview";
22
+ //# sourceMappingURL=livePreviewScript.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"livePreviewScript.d.ts","sourceRoot":"","sources":["../../src/proxy/livePreviewScript.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AASH;;;;;;;;;;;;GAYG;AACH,wBAAgB,2BAA2B,IAAI,MAAM,CAKpD;AAED,oEAAoE;AACpE,eAAO,MAAM,0BAA0B,sBAAsB,CAAC"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Copyright (c) 2026, Salesforce, Inc.,
3
+ * All rights reserved.
4
+ * For full license text, see the LICENSE.txt file
5
+ */
6
+ import { readFileSync } from "node:fs";
7
+ import { dirname, join } from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+ const __dirname = dirname(fileURLToPath(import.meta.url));
10
+ let cached = null;
11
+ /**
12
+ * Returns the JavaScript source that gets injected into previewed web apps.
13
+ * Reads from templates/livePreviewScript.js at runtime (copied to dist/ by postbuild).
14
+ * Cached after first read.
15
+ *
16
+ * Responsibilities:
17
+ * - Fetch interceptor for network-error detection (runs synchronously on load)
18
+ * - Runtime / compile / HMR error listeners
19
+ * - Error deduplication
20
+ * - postMessage bridge to the VS Code webview (when running inside an iframe)
21
+ * - Standalone browser error overlay (when NOT inside an iframe)
22
+ * - Copy / paste / right-click bridge for VS Code webview
23
+ */
24
+ export function getLivePreviewScriptContent() {
25
+ if (cached)
26
+ return cached;
27
+ const scriptPath = join(__dirname, "templates", "livePreviewScript.js");
28
+ cached = readFileSync(scriptPath, "utf-8");
29
+ return cached;
30
+ }
31
+ /** Data attribute used to detect (and prevent) double injection. */
32
+ export const LIVE_PREVIEW_SCRIPT_MARKER = "data-live-preview";
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Copyright (c) 2026, Salesforce, Inc.,
3
+ * All rights reserved.
4
+ * For full license text, see the LICENSE.txt file
5
+ */
6
+ export {};
7
+ //# sourceMappingURL=livePreviewScript.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"livePreviewScript.test.d.ts","sourceRoot":"","sources":["../../src/proxy/livePreviewScript.test.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Copyright (c) 2026, Salesforce, Inc.,
3
+ * All rights reserved.
4
+ * For full license text, see the LICENSE.txt file
5
+ */
6
+ import { describe, it, expect } from "vitest";
7
+ import { injectLivePreviewScript } from "./handler.js";
8
+ import { getLivePreviewScriptContent, LIVE_PREVIEW_SCRIPT_MARKER } from "./livePreviewScript.js";
9
+ describe("getLivePreviewScriptContent", () => {
10
+ it("should return a non-empty string", () => {
11
+ const content = getLivePreviewScriptContent();
12
+ expect(content).toBeTruthy();
13
+ expect(typeof content).toBe("string");
14
+ expect(content.length).toBeGreaterThan(100);
15
+ });
16
+ it("should contain the fetch interceptor", () => {
17
+ const content = getLivePreviewScriptContent();
18
+ expect(content).toContain("setupFetchInterceptorImmediate");
19
+ expect(content).toContain("_vscodeIntercepted");
20
+ });
21
+ it("should contain the error deduplication logic", () => {
22
+ const content = getLivePreviewScriptContent();
23
+ expect(content).toContain("ERROR_DEDUP_WINDOW_MS");
24
+ expect(content).toContain("getErrorHash");
25
+ expect(content).toContain("recentErrors");
26
+ });
27
+ it("should contain the sendErrorToParent function", () => {
28
+ const content = getLivePreviewScriptContent();
29
+ expect(content).toContain("sendErrorToParent");
30
+ });
31
+ it("should contain the error overlay function", () => {
32
+ const content = getLivePreviewScriptContent();
33
+ expect(content).toContain("showErrorOverlayInPage");
34
+ expect(content).toContain("vscode-error-panel");
35
+ });
36
+ it("should contain the copy/paste bridge", () => {
37
+ const content = getLivePreviewScriptContent();
38
+ expect(content).toContain("sendCopyMessage");
39
+ expect(content).toContain('command: "copy"');
40
+ });
41
+ it("should contain the iframeAlive heartbeat", () => {
42
+ const content = getLivePreviewScriptContent();
43
+ expect(content).toContain("iframeAlive");
44
+ });
45
+ it("should contain error listeners (window.error, unhandledrejection, console.error)", () => {
46
+ const content = getLivePreviewScriptContent();
47
+ expect(content).toContain('window.addEventListener(\n\t\t\t"error"');
48
+ expect(content).toContain('window.addEventListener("unhandledrejection"');
49
+ expect(content).toContain("console.error = function");
50
+ });
51
+ it("should wrap error details in a metadata object for postMessage", () => {
52
+ const content = getLivePreviewScriptContent();
53
+ expect(content).toContain("metadata:");
54
+ expect(content).toContain("metadata: {");
55
+ });
56
+ });
57
+ describe("LIVE_PREVIEW_SCRIPT_MARKER", () => {
58
+ it("should be a non-empty string", () => {
59
+ expect(LIVE_PREVIEW_SCRIPT_MARKER).toBeTruthy();
60
+ expect(typeof LIVE_PREVIEW_SCRIPT_MARKER).toBe("string");
61
+ });
62
+ });
63
+ describe("injectLivePreviewScript", () => {
64
+ it("should inject script before </body>", () => {
65
+ const html = "<html><body><div>Hello</div></body></html>";
66
+ const result = injectLivePreviewScript(html);
67
+ expect(result).toContain(LIVE_PREVIEW_SCRIPT_MARKER);
68
+ expect(result.indexOf(LIVE_PREVIEW_SCRIPT_MARKER)).toBeLessThan(result.indexOf("</body>"));
69
+ });
70
+ it("should inject before </html> when no </body> is present", () => {
71
+ const html = "<html><div>Hello</div></html>";
72
+ const result = injectLivePreviewScript(html);
73
+ expect(result).toContain(LIVE_PREVIEW_SCRIPT_MARKER);
74
+ expect(result.indexOf(LIVE_PREVIEW_SCRIPT_MARKER)).toBeLessThan(result.indexOf("</html>"));
75
+ });
76
+ it("should append at the end when no </body> or </html>", () => {
77
+ const html = "<div>Hello</div>";
78
+ const result = injectLivePreviewScript(html);
79
+ expect(result).toContain(LIVE_PREVIEW_SCRIPT_MARKER);
80
+ expect(result).toContain("<div>Hello</div>");
81
+ expect(result.endsWith("</script>")).toBe(true);
82
+ });
83
+ it("should prevent double injection", () => {
84
+ const html = "<html><body><div>Hello</div></body></html>";
85
+ const first = injectLivePreviewScript(html);
86
+ const second = injectLivePreviewScript(first);
87
+ expect(first).toBe(second);
88
+ });
89
+ it("should wrap script in <script> tags", () => {
90
+ const html = "<html><body></body></html>";
91
+ const result = injectLivePreviewScript(html);
92
+ expect(result).toContain("<script>");
93
+ expect(result).toContain("</script>");
94
+ });
95
+ it("should contain actual script content in the output", () => {
96
+ const html = "<html><body></body></html>";
97
+ const result = injectLivePreviewScript(html);
98
+ expect(result).toContain("sendErrorToParent");
99
+ expect(result).toContain("setupFetchInterceptorImmediate");
100
+ });
101
+ });
@@ -0,0 +1,738 @@
1
+ /**
2
+ * Copyright (c) 2026, Salesforce, Inc.,
3
+ * All rights reserved.
4
+ * For full license text, see the LICENSE.txt file
5
+ */
6
+
7
+ /* eslint-disable */
8
+ // Live Preview injected script — loaded at runtime by livePreviewScript.ts via readFileSync.
9
+ // This file runs in the browser (injected into <script> tags), NOT in Node.js.
10
+ // Live Preview postMessage contract (match extension TypeScript type):
11
+ // type: 'runtime' | 'compile' | 'network' | 'hmr' - extension uses for Fix Code vs Refresh Panel vs Re-authenticate
12
+ // For network: always send status (401/403 = auth, 0 = connection/socket failure)
13
+ // CRITICAL: Set up fetch interceptor IMMEDIATELY (synchronously, before defer)
14
+ // This ensures we catch all network errors, even those that happen during page load
15
+ (function setupFetchInterceptorImmediate() {
16
+ if (window.fetch && !window.fetch._vscodeIntercepted) {
17
+ const originalFetch = window.fetch;
18
+ window.fetch = function () {
19
+ const args = Array.prototype.slice.call(arguments);
20
+ const url = typeof args[0] === "string" ? args[0] : args[0] && args[0].url ? args[0].url : "";
21
+ const method = (args[1] && args[1].method ? args[1].method : "GET").toUpperCase();
22
+ const startTime = Date.now();
23
+
24
+ return originalFetch
25
+ .apply(window, args)
26
+ .then(function (response) {
27
+ const duration = Date.now() - startTime;
28
+
29
+ if (response.status >= 400) {
30
+ const errorData = {
31
+ type: "network",
32
+ method: method,
33
+ url: url,
34
+ status: response.status,
35
+ duration: duration,
36
+ message: "Network error: " + response.status + " " + (response.statusText || ""),
37
+ };
38
+ // Use sendErrorToParent (via window.__vscodeLivePreviewSendError) so dedup applies.
39
+ // Do NOT use console.error here — it would trigger the override and double-send.
40
+ if (typeof window.__vscodeLivePreviewSendError === "function") {
41
+ try {
42
+ window.__vscodeLivePreviewSendError(errorData);
43
+ } catch (err) {}
44
+ } else if (window.parent && window.parent !== window) {
45
+ // Fallback before init: send directly (no dedup yet)
46
+ try {
47
+ window.parent.postMessage(
48
+ {
49
+ type: errorData.type,
50
+ message: errorData.message,
51
+ metadata: {
52
+ method: errorData.method,
53
+ url: errorData.url,
54
+ status: errorData.status,
55
+ duration: errorData.duration,
56
+ },
57
+ _source: "webapps-proxy-injected-script",
58
+ },
59
+ "*",
60
+ );
61
+ } catch (e) {}
62
+ }
63
+ }
64
+
65
+ return response;
66
+ })
67
+ .catch(function (error) {
68
+ const duration = Date.now() - startTime;
69
+ const status =
70
+ (error && error.status) || (error && error.response && error.response.status) || 0;
71
+ const errorData = {
72
+ type: "network",
73
+ method: method,
74
+ url: url,
75
+ status: status,
76
+ duration: duration,
77
+ message:
78
+ error && error.message
79
+ ? error.message
80
+ : status > 0
81
+ ? "Network error: " + status
82
+ : "Network request failed",
83
+ stack: error && error.stack ? error.stack : "",
84
+ };
85
+ // Use sendErrorToParent (via window.__vscodeLivePreviewSendError) so dedup applies.
86
+ // Do NOT use console.error — it would trigger the override and double-send.
87
+ if (typeof window.__vscodeLivePreviewSendError === "function") {
88
+ try {
89
+ window.__vscodeLivePreviewSendError(errorData);
90
+ } catch (err) {}
91
+ } else if (window.parent && window.parent !== window) {
92
+ try {
93
+ window.parent.postMessage(
94
+ {
95
+ type: errorData.type,
96
+ message: errorData.message,
97
+ stack: errorData.stack,
98
+ metadata: {
99
+ method: errorData.method,
100
+ url: errorData.url,
101
+ status: errorData.status,
102
+ duration: errorData.duration,
103
+ },
104
+ _source: "webapps-proxy-injected-script",
105
+ },
106
+ "*",
107
+ );
108
+ } catch (e) {}
109
+ }
110
+ throw error;
111
+ });
112
+ };
113
+ window.fetch._vscodeIntercepted = true;
114
+ }
115
+ })();
116
+ (function () {
117
+ // Defer script execution to avoid blocking initial page render
118
+ // Use requestIdleCallback if available, otherwise setTimeout
119
+ function deferInit(callback) {
120
+ if (window.requestIdleCallback) {
121
+ window.requestIdleCallback(callback, { timeout: 100 });
122
+ } else {
123
+ setTimeout(callback, 0);
124
+ }
125
+ }
126
+ function initVSCodeLivePreview() {
127
+ const recentErrors = {};
128
+ const ERROR_DEDUP_WINDOW_MS = 2000;
129
+
130
+ function safeStr(val, maxLen) {
131
+ if (val == null) return "";
132
+ const s = typeof val === "string" ? val : String(val);
133
+ return s.substring(0, maxLen == null ? s.length : maxLen);
134
+ }
135
+
136
+ function getErrorHash(errorData) {
137
+ if (errorData.type === "network") {
138
+ return (
139
+ (errorData.type || "unknown") +
140
+ "|" +
141
+ (errorData.status || "0") +
142
+ "|" +
143
+ safeStr(errorData.url, 100) +
144
+ "|" +
145
+ (errorData.method || "GET")
146
+ );
147
+ }
148
+ return (
149
+ (errorData.type || "unknown") +
150
+ "|" +
151
+ safeStr(errorData.message, 100) +
152
+ "|" +
153
+ safeStr(errorData.source, 50)
154
+ );
155
+ }
156
+
157
+ function escapeHtml(text) {
158
+ if (!text) return "";
159
+ const div = document.createElement("div");
160
+ div.textContent = text;
161
+ return div.innerHTML;
162
+ }
163
+
164
+ // Extracts filename from error data. Mirrors logic in errorPathUtils.ts (extension)
165
+ // and uiPreviewWebview.js (webview) — keep patterns in sync across all three.
166
+ function extractFileName(errorData) {
167
+ const combined =
168
+ (errorData.message || "") + " " + (errorData.source || "") + " " + (errorData.stack || "");
169
+ const patterns = [
170
+ /Failed to reload\s+\/?@?fs?\/?([^\s:?)]+\.[jt]sx?)/,
171
+ /@fs\/([^\s:?)]+\.[jt]sx?)/,
172
+ /\/(src\/[^\s:?)]+\.[jt]sx?)/,
173
+ /\/([^\s\/:?)]+\.[jt]sx?)[\s:?]/,
174
+ /at\s+\w+\s+\([^)]*\/([^\s\/:?)]+\.[jt]sx?)/,
175
+ ];
176
+ for (let i = 0; i < patterns.length; i++) {
177
+ const m = combined.match(patterns[i]);
178
+ if (m && m[1]) {
179
+ const parts = m[1].split("/");
180
+ return parts[parts.length - 1];
181
+ }
182
+ }
183
+ return null;
184
+ }
185
+
186
+ function getFileIconLabel(name) {
187
+ if (!name) return "JS";
188
+ const ext = name.split(".").pop().toLowerCase();
189
+ if (ext === "ts" || ext === "tsx") return "TS";
190
+ if (ext === "jsx") return "JSX";
191
+ return "JS";
192
+ }
193
+
194
+ // SVG illustration for error overlay (Figma design asset)
195
+ const ERROR_OVERLAY_SVG =
196
+ '<svg width="300" height="192" viewBox="0 0 300 192" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M104.698 144.786C104.458 144.138 103.542 144.138 103.302 144.786L101.757 148.962C101.621 149.33 101.33 149.621 100.962 149.757L96.7861 151.302C96.1381 151.542 96.1381 152.458 96.7861 152.698L100.962 154.243C101.33 154.379 101.621 154.67 101.757 155.038L103.302 159.214C103.542 159.862 104.458 159.862 104.698 159.214L106.243 155.038C106.379 154.67 106.67 154.379 107.038 154.243L111.214 152.698C111.862 152.458 111.862 151.542 111.214 151.302L107.038 149.757C106.67 149.621 106.379 149.33 106.243 148.962L104.698 144.786Z" stroke="#757575" stroke-width="0.6"/><path d="M57.265 44.5117C57.5175 43.8294 58.4825 43.8294 58.735 44.5117L59.8937 47.6433C59.9731 47.8578 60.1422 48.0269 60.3567 48.1063L63.4883 49.265C64.1706 49.5175 64.1706 50.4825 63.4883 50.735L60.3567 51.8937C60.1422 51.9731 59.9731 52.1422 59.8937 52.3567L58.735 55.4883C58.4825 56.1706 57.5175 56.1706 57.265 55.4883L56.1063 52.3567C56.0269 52.1422 55.8578 51.9731 55.6433 51.8937L52.5117 50.735C51.8294 50.4825 51.8294 49.5175 52.5117 49.265L55.6433 48.1063C55.8578 48.0269 56.0269 47.8578 56.1063 47.6433L57.265 44.5117Z" fill="#AEAEAE"/><path d="M236.477 128.332C236.313 127.889 235.687 127.889 235.523 128.332L234.742 130.442C234.691 130.581 234.581 130.691 234.442 130.742L232.332 131.523C231.889 131.687 231.889 132.313 232.332 132.477L234.442 133.258C234.581 133.309 234.691 133.419 234.742 133.558L235.523 135.668C235.687 136.111 236.313 136.111 236.477 135.668L237.258 133.558C237.309 133.419 237.419 133.309 237.558 133.258L239.668 132.477C240.111 132.313 240.111 131.687 239.668 131.523L237.558 130.742C237.419 130.691 237.309 130.581 237.258 130.442L236.477 128.332Z" fill="#AEAEAE"/><rect x="160" y="106" width="2" height="16" transform="rotate(-90 160 106)" fill="#7CB1FE"/><rect x="160" y="88" width="2" height="16" transform="rotate(-90 160 88)" fill="#7CB1FE"/><path d="M56 96C56 89.3726 61.3726 84 68 84L75 84C75 90.6274 69.6274 96 63 96L56 96Z" fill="#066AFE"/><path d="M264 48C257.373 48 252 42.6274 252 36L252 29C258.627 29 264 34.3726 264 41L264 48Z" fill="#066AFE"/><path d="M56 96C56 91.5818 52.4183 88 48 88C48 92.4183 51.5817 96 56 96Z" fill="#7CB1FE"/><path d="M264 48C259.582 48 256 51.5817 256 56C260.418 56 264 52.4183 264 48Z" fill="#7CB1FE"/><path d="M40 152C53.2548 152 64 141.255 64 128C50.7452 128 40 138.745 40 152Z" fill="#066AFE"/><path d="M264 120C250.745 120 240 109.255 240 96C253.255 96 264 106.745 264 120Z" fill="#066AFE"/><path d="M88 96L60 96C48.9543 96 40 104.954 40 116L40 168" stroke="#7CB1FE"/><path d="M216 96H244C255.046 96 264 87.0457 264 76V24" stroke="#7CB1FE"/><path d="M128 64L128 128L112 128C98.7452 128 88 117.255 88 104L88 88C88 74.7452 98.7452 64 112 64L128 64Z" fill="#066AFE"/><path d="M176 128L176 64L192 64C205.255 64 216 74.7452 216 88L216 104C216 117.255 205.255 128 192 128L176 128Z" fill="url(#pg1)"/><defs><linearGradient id="pg1" x1="176" y1="67.5" x2="217.94" y2="86.14" gradientUnits="userSpaceOnUse"><stop stop-color="#7CB1FE"/><stop offset="1" stop-color="#066AFE"/></linearGradient></defs></svg>';
197
+
198
+ const COPY_FEEDBACK_DELAY_MS = 1500;
199
+
200
+ function injectOverlayStyles() {
201
+ if (document.getElementById("vscode-error-overlay-styles")) return;
202
+ const style = document.createElement("style");
203
+ style.id = "vscode-error-overlay-styles";
204
+ style.textContent = [
205
+ '#vscode-error-panel{position:fixed;top:0;left:0;right:0;bottom:0;background:#181818;z-index:999999;overflow-y:auto;display:flex;flex-direction:column;justify-content:center;align-items:center;padding:0 0 30px;font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","Segoe UI",system-ui,sans-serif}',
206
+ ".vep-content{display:flex;flex-direction:column;justify-content:center;align-items:center;gap:46px}",
207
+ ".vep-top{display:flex;flex-direction:column;align-items:center;gap:16px}",
208
+ ".vep-illustration{width:300px;height:192px}",
209
+ ".vep-title{display:flex;align-items:center;gap:8px;padding-bottom:8px}",
210
+ ".vep-title-icon{width:16px;height:16px;color:#FE8AA7;font-size:16px;display:flex;align-items:center;justify-content:center}",
211
+ ".vep-title-text{font-weight:600;font-size:20px;line-height:26px;color:#FE8AA7}",
212
+ ".vep-body{display:flex;flex-direction:column;align-items:center;gap:24px;width:416px}",
213
+ ".vep-fields{display:flex;flex-direction:column;gap:16px;width:100%}",
214
+ ".vep-field{display:flex;flex-direction:column;gap:8px;width:100%}",
215
+ ".vep-label{font-weight:500;font-size:14px;line-height:17px;color:#FFFFFF}",
216
+ ".vep-file-box{box-sizing:border-box;display:flex;align-items:center;padding:4px 8px;gap:4px;width:100%;background:#181818;border:1px solid #444444;border-radius:4px}",
217
+ ".vep-file-icon{font-size:10px;font-weight:700;color:#F9E3B6;letter-spacing:0.3px}",
218
+ ".vep-file-name{font-weight:500;font-size:13px;line-height:16px;color:#AEAEAE;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
219
+ ".vep-error-box{box-sizing:border-box;display:flex;align-items:center;padding:8px;gap:4px;width:100%;background:#242424;border:1px solid #FE8AA7;border-radius:8px}",
220
+ ".vep-error-icon{width:16px;height:16px;color:#FE8AA7;font-size:16px;flex-shrink:0;display:flex;align-items:center;justify-content:center}",
221
+ ".vep-error-msg{font-weight:500;font-size:12px;line-height:14px;color:#FE8AA7;word-break:break-word}",
222
+ ".vep-stack-box{box-sizing:border-box;padding:8px;width:100%;max-height:200px;overflow-y:auto;background:#181818;border:1px solid #444444;border-radius:4px;font-weight:500;font-size:12px;line-height:14px;color:#AEAEAE;white-space:pre-wrap;word-break:break-word;user-select:text}",
223
+ ".vep-actions{display:flex;gap:8px;width:100%}",
224
+ ".vep-btn{display:flex;align-items:center;gap:4px;padding:2px 4px;background:#066AFE;border-radius:4px;border:none;cursor:pointer;font-weight:500;font-size:11px;line-height:13px;color:#fff;white-space:nowrap}",
225
+ ".vep-btn:hover{background:#0559d4}",
226
+ ].join("\n");
227
+ document.head.appendChild(style);
228
+ }
229
+
230
+ function showErrorOverlayInPage(errorData) {
231
+ try {
232
+ const existing = document.getElementById("vscode-error-panel");
233
+ if (existing) existing.remove();
234
+
235
+ injectOverlayStyles();
236
+
237
+ const errorType = errorData.type || "runtime";
238
+ const titleMap = {
239
+ network: "Network Error",
240
+ compile: "Compilation Error",
241
+ hmr: "HMR Error",
242
+ component: "Component Error",
243
+ };
244
+ const titleText = titleMap[errorType] || "Runtime Error";
245
+ const fileName = extractFileName(errorData);
246
+ const fileIcon = getFileIconLabel(fileName);
247
+
248
+ const panel = document.createElement("div");
249
+ panel.id = "vscode-error-panel";
250
+
251
+ const fileHtml = fileName
252
+ ? '<div class="vep-field"><div class="vep-label">File</div>' +
253
+ '<div class="vep-file-box"><span class="vep-file-icon">' +
254
+ escapeHtml(fileIcon) +
255
+ "</span>" +
256
+ '<span class="vep-file-name">' +
257
+ escapeHtml(fileName) +
258
+ "</span></div></div>"
259
+ : "";
260
+
261
+ const stackHtml =
262
+ errorData.stack && errorData.stack.trim()
263
+ ? '<div class="vep-field"><div class="vep-label">Stack Trace</div>' +
264
+ '<div class="vep-stack-box">' +
265
+ escapeHtml(errorData.stack) +
266
+ "</div></div>"
267
+ : "";
268
+
269
+ panel.innerHTML =
270
+ '<div class="vep-content">' +
271
+ '<div class="vep-top">' +
272
+ '<div class="vep-illustration">' +
273
+ ERROR_OVERLAY_SVG +
274
+ "</div>" +
275
+ '<div class="vep-title"><span class="vep-title-icon">\u2297</span>' +
276
+ '<span class="vep-title-text">' +
277
+ escapeHtml(titleText) +
278
+ "</span></div>" +
279
+ "</div>" +
280
+ '<div class="vep-body">' +
281
+ '<div class="vep-fields">' +
282
+ fileHtml +
283
+ '<div class="vep-field"><div class="vep-label">Error</div>' +
284
+ '<div class="vep-error-box"><span class="vep-error-icon">\u2297</span>' +
285
+ '<span class="vep-error-msg">' +
286
+ escapeHtml(errorData.message || "Unknown error") +
287
+ "</span></div></div>" +
288
+ stackHtml +
289
+ "</div>" +
290
+ '<div class="vep-actions">' +
291
+ '<button class="vep-btn" id="vep-copy-btn">\u{1F4CB} Copy Error</button>' +
292
+ '<button class="vep-btn" id="vep-retry-btn">\u21BB Retry</button>' +
293
+ "</div>" +
294
+ "</div>" +
295
+ "</div>";
296
+
297
+ document.body.appendChild(panel);
298
+
299
+ document.getElementById("vep-copy-btn").onclick = function () {
300
+ const parts = [titleText, ""];
301
+ if (fileName) parts.push("File: " + fileName);
302
+ parts.push("Error: " + (errorData.message || "Unknown error"));
303
+ if (errorData.stack && errorData.stack.trim()) {
304
+ parts.push("");
305
+ parts.push("Stack Trace:");
306
+ parts.push(errorData.stack);
307
+ }
308
+ const text = parts.join("\n");
309
+ const btn = this;
310
+ if (navigator.clipboard && navigator.clipboard.writeText) {
311
+ navigator.clipboard
312
+ .writeText(text)
313
+ .then(function () {
314
+ btn.innerHTML = "\u2714 Copied!";
315
+ setTimeout(function () {
316
+ btn.innerHTML = "\u{1F4CB} Copy Error";
317
+ }, COPY_FEEDBACK_DELAY_MS);
318
+ })
319
+ .catch(function () {
320
+ btn.innerHTML = "Copy failed";
321
+ });
322
+ }
323
+ };
324
+ document.getElementById("vep-retry-btn").onclick = function () {
325
+ panel.remove();
326
+ window.location.reload();
327
+ };
328
+
329
+ document.addEventListener("keydown", function onEsc(e) {
330
+ if (e.key === "Escape") {
331
+ panel.remove();
332
+ document.removeEventListener("keydown", onEsc);
333
+ }
334
+ });
335
+ } catch (e) {
336
+ console.error("[webapps-proxy] showErrorOverlayInPage failed:", e);
337
+ }
338
+ }
339
+
340
+ function sendErrorToParent(errorData) {
341
+ const errorHash = getErrorHash(errorData);
342
+ const now = Date.now();
343
+
344
+ if (recentErrors[errorHash]) {
345
+ const timeSinceLastSend = now - recentErrors[errorHash];
346
+ if (timeSinceLastSend < ERROR_DEDUP_WINDOW_MS) {
347
+ return;
348
+ }
349
+ }
350
+
351
+ recentErrors[errorHash] = now;
352
+
353
+ const errorKeys = Object.keys(recentErrors);
354
+ if (errorKeys.length > 50) {
355
+ const sortedKeys = errorKeys.sort(function (a, b) {
356
+ return recentErrors[a] - recentErrors[b];
357
+ });
358
+ for (let i = 0; i < sortedKeys.length - 50; i++) {
359
+ delete recentErrors[sortedKeys[i]];
360
+ }
361
+ }
362
+
363
+ if (window.parent && window.parent !== window) {
364
+ try {
365
+ const msg = {
366
+ type: errorData.type,
367
+ message: errorData.message,
368
+ stack: errorData.stack,
369
+ metadata: {
370
+ source: errorData.source || window.location.href,
371
+ level: errorData.level,
372
+ method: errorData.method,
373
+ url: errorData.url,
374
+ status: errorData.status,
375
+ duration: errorData.duration,
376
+ },
377
+ _source: "webapps-proxy-injected-script",
378
+ };
379
+ window.parent.postMessage(msg, "*");
380
+ } catch (err) {
381
+ console.error("[webapps-proxy] Failed to send error to parent:", err);
382
+ }
383
+ } else {
384
+ showErrorOverlayInPage(errorData);
385
+ }
386
+ }
387
+ // Expose for fetch interceptor: once init runs, network errors use sendErrorToParent (dedup)
388
+ try {
389
+ window.__vscodeLivePreviewSendError = sendErrorToParent;
390
+ } catch (e) {}
391
+
392
+ const initTime = new Date().toISOString();
393
+ if (window.parent && window.parent !== window) {
394
+ try {
395
+ window.parent.postMessage(
396
+ {
397
+ command: "iframeAlive",
398
+ version: "2026-01-19-v5",
399
+ timestamp: initTime,
400
+ source: "webapps-proxy-injected-script",
401
+ },
402
+ "*",
403
+ );
404
+ } catch (e) {
405
+ console.error("[webapps-proxy] Failed to send iframeAlive message:", e);
406
+ }
407
+ }
408
+ if (!document.getElementById("vscode-text-selection-style")) {
409
+ const style = document.createElement("style");
410
+ style.id = "vscode-text-selection-style";
411
+ style.textContent =
412
+ "* { -webkit-user-select: text !important; user-select: text !important; }";
413
+ document.head.appendChild(style);
414
+ }
415
+
416
+ let lastSelection = "";
417
+ let lastCopyTime = 0;
418
+ const COPY_THROTTLE_MS = 100;
419
+
420
+ // Function to get current selection and send copy message
421
+ function sendCopyMessage(forceCopy) {
422
+ if (forceCopy === undefined) forceCopy = false;
423
+ const timestamp = new Date().toISOString();
424
+ const now = Date.now();
425
+ const timeSinceLastCopy = now - lastCopyTime;
426
+ let selection = window.getSelection();
427
+ let text = "";
428
+
429
+ if (selection && selection.toString().trim()) {
430
+ text = selection.toString();
431
+ } else {
432
+ try {
433
+ const docSelection = document.getSelection();
434
+ if (docSelection && docSelection.toString().trim()) {
435
+ text = docSelection.toString();
436
+ }
437
+ } catch (e) {}
438
+ }
439
+ if (!text || text.length === 0) {
440
+ try {
441
+ const activeElement = document.activeElement;
442
+ if (
443
+ activeElement &&
444
+ (activeElement.tagName === "INPUT" || activeElement.tagName === "TEXTAREA")
445
+ ) {
446
+ const input = activeElement;
447
+ if (input.selectionStart !== undefined && input.selectionEnd !== undefined) {
448
+ text = input.value.substring(input.selectionStart, input.selectionEnd);
449
+ }
450
+ }
451
+ } catch (e) {}
452
+ }
453
+ if (!text || text.trim().length === 0) {
454
+ return;
455
+ }
456
+
457
+ const selectionChanged = text !== lastSelection;
458
+ const shouldThrottle = !forceCopy && timeSinceLastCopy < COPY_THROTTLE_MS;
459
+
460
+ if (forceCopy || (selectionChanged && !shouldThrottle)) {
461
+ lastSelection = text;
462
+ lastCopyTime = now;
463
+
464
+ if (window.parent && window.parent !== window) {
465
+ try {
466
+ const message = {
467
+ command: "copy",
468
+ text: text,
469
+ timestamp: timestamp,
470
+ };
471
+ window.parent.postMessage(message, "*");
472
+ } catch (err) {
473
+ console.error("[webapps-proxy] Copy: Error sending postMessage:", err);
474
+ }
475
+ }
476
+ }
477
+ }
478
+
479
+ // Listen for keyboard shortcuts (Cmd+C / Ctrl+C) - user-initiated, always send
480
+ document.addEventListener(
481
+ "keydown",
482
+ function (e) {
483
+ // Cmd+C (Mac) or Ctrl+C (Windows/Linux)
484
+ if ((e.metaKey || e.ctrlKey) && (e.key === "c" || e.key === "C" || e.keyCode === 67)) {
485
+ // Small delay to ensure selection is captured after VS Code processes the event
486
+ setTimeout(function () {
487
+ sendCopyMessage(true); // forceCopy = true for user-initiated actions
488
+ }, 0);
489
+ }
490
+ },
491
+ true,
492
+ ); // Use capture phase - highest priority
493
+
494
+ // Listen for copy events as backup
495
+ document.addEventListener(
496
+ "copy",
497
+ function (e) {
498
+ // Also force send on copy event (user-initiated)
499
+ setTimeout(function () {
500
+ sendCopyMessage(true);
501
+ }, 0);
502
+ },
503
+ true,
504
+ ); // Use capture phase to catch before VS Code intercepts
505
+
506
+ document.addEventListener(
507
+ "contextmenu",
508
+ function (e) {
509
+ // Prevent default browser context menu so our custom one shows
510
+ e.preventDefault();
511
+ const selection = window.getSelection();
512
+ const selectedText = selection && selection.toString().trim() ? selection.toString() : "";
513
+ if (window.parent && window.parent !== window) {
514
+ try {
515
+ window.parent.postMessage(
516
+ {
517
+ command: "rightClick",
518
+ x: e.clientX,
519
+ y: e.clientY,
520
+ selectedText: selectedText,
521
+ },
522
+ "*",
523
+ );
524
+ } catch (err) {
525
+ console.error("[webapps-proxy] Right-click: Error sending postMessage:", err);
526
+ }
527
+ }
528
+ },
529
+ true,
530
+ );
531
+
532
+ window.addEventListener("message", function (e) {
533
+ if (e.data && e.data.command === "getSelection") {
534
+ const selection = window.getSelection();
535
+ const text = selection && selection.toString() ? selection.toString() : "";
536
+ if (window.parent && window.parent !== window) {
537
+ window.parent.postMessage({ command: "selectionText", text: text }, "*");
538
+ }
539
+ } else if (e.data && e.data.command === "selectAll") {
540
+ document.execCommand("selectAll", false, null);
541
+ } else if (e.data && e.data.command === "paste") {
542
+ // Paste text into active element or document
543
+ if (e.data.text) {
544
+ try {
545
+ const activeElement = document.activeElement;
546
+ if (
547
+ activeElement &&
548
+ (activeElement.tagName === "INPUT" ||
549
+ activeElement.tagName === "TEXTAREA" ||
550
+ activeElement.isContentEditable)
551
+ ) {
552
+ if (activeElement.tagName === "INPUT" || activeElement.tagName === "TEXTAREA") {
553
+ const input = activeElement;
554
+ const start = input.selectionStart || 0;
555
+ const end = input.selectionEnd || 0;
556
+ const value = input.value || "";
557
+ input.value = value.substring(0, start) + e.data.text + value.substring(end);
558
+ input.selectionStart = input.selectionEnd = start + e.data.text.length;
559
+ input.dispatchEvent(new Event("input", { bubbles: true }));
560
+ } else if (activeElement.isContentEditable) {
561
+ document.execCommand("insertText", false, e.data.text);
562
+ }
563
+ } else {
564
+ // Fallback: try to paste into body
565
+ document.execCommand("insertText", false, e.data.text);
566
+ }
567
+ } catch (err) {
568
+ console.error("[webapps-proxy] paste: Failed to paste text:", err);
569
+ }
570
+ }
571
+ }
572
+ });
573
+
574
+ // Send click events to parent to hide context menu when clicking elsewhere
575
+ document.addEventListener(
576
+ "click",
577
+ function (e) {
578
+ if (window.parent && window.parent !== window) {
579
+ try {
580
+ window.parent.postMessage(
581
+ {
582
+ command: "iframeClick",
583
+ x: e.clientX,
584
+ y: e.clientY,
585
+ },
586
+ "*",
587
+ );
588
+ } catch (err) {
589
+ // Silently fail if postMessage fails
590
+ }
591
+ }
592
+ },
593
+ true,
594
+ );
595
+
596
+ // 1. Handle unhandled JavaScript errors (runtime - extension shows Refresh Panel)
597
+ window.addEventListener(
598
+ "error",
599
+ function (event) {
600
+ const errorData = {
601
+ type: "runtime",
602
+ message: event.message || "Unknown error",
603
+ stack: event.error ? event.error.stack : "",
604
+ source: event.filename || window.location.href,
605
+ };
606
+ console.error("[webapps-proxy] Unhandled error detected:", errorData);
607
+ sendErrorToParent(errorData);
608
+ },
609
+ true,
610
+ );
611
+
612
+ // 2. Handle unhandled promise rejections (runtime or network - extension shows appropriate actions)
613
+ window.addEventListener("unhandledrejection", function (event) {
614
+ const reason = event.reason;
615
+ const msg =
616
+ reason && reason.message ? reason.message : String(reason) || "Unhandled promise rejection";
617
+ // "Failed to fetch" and similar come from fetch() rejections (DNS, CORS, connection) — classify as network
618
+ const isNetworkFailure =
619
+ /failed to fetch|network request failed|load failed|networkerror/i.test(msg);
620
+ const errorData = {
621
+ type: isNetworkFailure ? "network" : "runtime",
622
+ message: msg,
623
+ stack: reason && reason.stack ? reason.stack : "",
624
+ source: window.location.href,
625
+ };
626
+ console.error("[webapps-proxy] Unhandled promise rejection detected:", errorData);
627
+ sendErrorToParent(errorData);
628
+ });
629
+
630
+ // 3. Override console.error to catch errors surfaced by frameworks (React, Vite).
631
+ // Trade-off: this catches all console.error calls (including third-party noise).
632
+ // The ignore list below filters known framework messages. A re-entrancy guard
633
+ // prevents infinite loops if sendErrorToParent itself triggers console.error.
634
+ const originalConsoleError = console.error;
635
+ let _inConsoleErrorOverride = false;
636
+ console.error = function () {
637
+ const args = Array.prototype.slice.call(arguments);
638
+ originalConsoleError.apply(console, args);
639
+ if (_inConsoleErrorOverride) return;
640
+ _inConsoleErrorOverride = true;
641
+ try {
642
+ let message = "";
643
+ let stack = "";
644
+
645
+ for (let i = 0; i < args.length; i++) {
646
+ const arg = args[i];
647
+ if (arg instanceof Error) {
648
+ message = arg.message;
649
+ stack = arg.stack || "";
650
+ break;
651
+ } else if (typeof arg === "string") {
652
+ message = arg;
653
+ } else if (arg && typeof arg === "object") {
654
+ message =
655
+ arg.message && typeof arg.message === "string" ? arg.message : JSON.stringify(arg);
656
+ } else {
657
+ message = String(arg);
658
+ }
659
+ }
660
+
661
+ // If no message extracted, stringify all args (avoid [object Object])
662
+ if (!message && args.length > 0) {
663
+ message = args
664
+ .map(function (arg) {
665
+ if (arg && typeof arg === "object" && !(arg instanceof Error)) {
666
+ return arg.message && typeof arg.message === "string"
667
+ ? arg.message
668
+ : JSON.stringify(arg);
669
+ }
670
+ return String(arg);
671
+ })
672
+ .join(" ");
673
+ }
674
+ // Skip sending noise: [object Object] with no useful content
675
+ if (message === "[object Object]") {
676
+ return;
677
+ }
678
+
679
+ const messageLower = (
680
+ typeof message === "string" ? message : String(message)
681
+ ).toLowerCase();
682
+ if (messageLower.includes("failed to reload")) {
683
+ const errorData = {
684
+ type: "compile",
685
+ level: "error",
686
+ message: (typeof message === "string" ? message : String(message)) || "Console error",
687
+ stack: stack,
688
+ };
689
+ sendErrorToParent(errorData);
690
+ return;
691
+ }
692
+ // Filter out other noisy/expected errors that shouldn't trigger notifications
693
+ const shouldIgnore =
694
+ messageLower.includes("hmr update") ||
695
+ messageLower.includes("fast refresh") ||
696
+ messageLower.includes("could not fast refresh") ||
697
+ (messageLower.includes("[vite]") && messageLower.includes("hmr")) ||
698
+ messageLower.includes("reactdomclient.createroot");
699
+
700
+ if (shouldIgnore) {
701
+ return; // Don't send these to VS Code
702
+ }
703
+
704
+ // Skip network errors: fetch interceptor already sends them (avoids double postMessage)
705
+ for (let i = 0; i < args.length; i++) {
706
+ if (args[i] && typeof args[i] === "object" && args[i].type === "network") {
707
+ return;
708
+ }
709
+ }
710
+
711
+ const errorData = {
712
+ type: "runtime",
713
+ level: "error",
714
+ message: message || "Console error",
715
+ stack: stack,
716
+ };
717
+
718
+ sendErrorToParent(errorData);
719
+ } finally {
720
+ _inConsoleErrorOverride = false;
721
+ }
722
+ };
723
+ }
724
+
725
+ // Defer execution to avoid blocking initial page render
726
+ // Wait for page to be interactive before initializing
727
+ if (document.readyState === "loading") {
728
+ document.addEventListener("DOMContentLoaded", function () {
729
+ deferInit(initVSCodeLivePreview);
730
+ });
731
+ } else if (document.readyState === "interactive") {
732
+ // DOM is interactive, defer to next idle period
733
+ deferInit(initVSCodeLivePreview);
734
+ } else {
735
+ // DOM is complete, run immediately but asynchronously
736
+ deferInit(initVSCodeLivePreview);
737
+ }
738
+ })();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@salesforce/webapp-experimental",
3
3
  "description": "[experimental] Core package for Salesforce Web Applications",
4
- "version": "1.72.0",
4
+ "version": "1.73.1",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "type": "module",
7
7
  "main": "./dist/index.js",
@@ -45,7 +45,7 @@
45
45
  },
46
46
  "dependencies": {
47
47
  "@salesforce/core": "^8.23.4",
48
- "@salesforce/sdk-data": "^1.72.0",
48
+ "@salesforce/sdk-data": "^1.73.1",
49
49
  "axios": "^1.7.7",
50
50
  "micromatch": "^4.0.8",
51
51
  "path-to-regexp": "^8.3.0"