diff --git a/webview-ui/src/index.tsx b/webview-ui/src/index.tsx index 4793c0272ac..76463aaf655 100644 --- a/webview-ui/src/index.tsx +++ b/webview-ui/src/index.tsx @@ -6,6 +6,11 @@ import App from "./App" import "../node_modules/@vscode/codicons/dist/codicon.css" import { getHighlighter } from "./utils/highlighter" +import { initServiceWorkerPrevention } from "./utils/serviceWorkerPrevention" + +// Prevent ServiceWorker registration in VS Code webview environment +// This must be done before any third-party libraries are initialized +initServiceWorkerPrevention() // Initialize Shiki early to hide initialization latency (async) getHighlighter().catch((error: Error) => console.error("Failed to initialize Shiki highlighter:", error)) diff --git a/webview-ui/src/utils/TelemetryClient.ts b/webview-ui/src/utils/TelemetryClient.ts index 7d684000036..c06707d7f65 100644 --- a/webview-ui/src/utils/TelemetryClient.ts +++ b/webview-ui/src/utils/TelemetryClient.ts @@ -20,6 +20,11 @@ class TelemetryClient { capture_pageview: false, capture_pageleave: false, autocapture: false, + // Disable features that might use ServiceWorkers + disable_persistence: false, // Keep localStorage persistence but avoid advanced features + bootstrap: {}, // Avoid bootstrapping that might trigger ServiceWorker + // Disable session recording which might use ServiceWorkers + disable_session_recording: true, }) } else { TelemetryClient.telemetryEnabled = false diff --git a/webview-ui/src/utils/__tests__/serviceWorkerPrevention.spec.ts b/webview-ui/src/utils/__tests__/serviceWorkerPrevention.spec.ts new file mode 100644 index 00000000000..7cfc20ba61e --- /dev/null +++ b/webview-ui/src/utils/__tests__/serviceWorkerPrevention.spec.ts @@ -0,0 +1,167 @@ +import { describe, it, expect, beforeEach, vi, afterEach } from "vitest" +import { + isVSCodeWebview, + disableServiceWorkerRegistration, + initServiceWorkerPrevention, +} from "../serviceWorkerPrevention" + +describe("serviceWorkerPrevention", () => { + let originalAcquireVsCodeApi: any + + beforeEach(() => { + // Store original values + originalAcquireVsCodeApi = (global as any).acquireVsCodeApi + + // Mock navigator.serviceWorker for each test + const mockServiceWorker = { + register: vi.fn(), + getRegistration: vi.fn(), + getRegistrations: vi.fn(), + ready: Promise.resolve(), + controller: null, + oncontrollerchange: null, + onmessage: null, + onmessageerror: null, + } + + // Use vi.stubGlobal for navigator + vi.stubGlobal("navigator", { + serviceWorker: mockServiceWorker, + }) + }) + + afterEach(() => { + // Restore original values + vi.unstubAllGlobals() + + if (originalAcquireVsCodeApi !== undefined) { + ;(global as any).acquireVsCodeApi = originalAcquireVsCodeApi + } else { + delete (global as any).acquireVsCodeApi + } + }) + + describe("isVSCodeWebview", () => { + it("should return true when acquireVsCodeApi is defined", () => { + // Mock VS Code webview environment + ;(global as any).acquireVsCodeApi = vi.fn() + + expect(isVSCodeWebview()).toBe(true) + }) + + it("should return false when acquireVsCodeApi is undefined", () => { + // Ensure it's undefined + delete (global as any).acquireVsCodeApi + + expect(isVSCodeWebview()).toBe(false) + }) + }) + + describe("disableServiceWorkerRegistration", () => { + it("should override navigator.serviceWorker.register in VS Code webview", async () => { + // Mock VS Code webview environment + ;(global as any).acquireVsCodeApi = vi.fn() + + disableServiceWorkerRegistration() + + // Try to register a service worker + await expect(navigator.serviceWorker.register("/sw.js")).rejects.toThrow( + "ServiceWorker registration is not allowed in VS Code webview", + ) + }) + + it("should not override navigator.serviceWorker when not in VS Code webview", () => { + // Ensure we're not in VS Code webview + delete (global as any).acquireVsCodeApi + + const originalRegister = navigator.serviceWorker.register + + disableServiceWorkerRegistration() + + // Should still be the original function + expect(navigator.serviceWorker.register).toBe(originalRegister) + }) + + it("should provide mock getRegistration that returns undefined", async () => { + // Mock VS Code webview environment + ;(global as any).acquireVsCodeApi = vi.fn() + + disableServiceWorkerRegistration() + + const registration = await navigator.serviceWorker.getRegistration() + expect(registration).toBeUndefined() + }) + + it("should provide mock getRegistrations that returns empty array", async () => { + // Mock VS Code webview environment + ;(global as any).acquireVsCodeApi = vi.fn() + + disableServiceWorkerRegistration() + + const registrations = await navigator.serviceWorker.getRegistrations() + expect(registrations).toEqual([]) + }) + + it("should handle missing navigator.serviceWorker gracefully", () => { + // Mock VS Code webview environment + ;(global as any).acquireVsCodeApi = vi.fn() + + // Remove serviceWorker from navigator + vi.stubGlobal("navigator", {}) + + // Should not throw + expect(() => disableServiceWorkerRegistration()).not.toThrow() + }) + }) + + describe("initServiceWorkerPrevention", () => { + it("should initialize without throwing errors", () => { + // Mock VS Code webview environment + ;(global as any).acquireVsCodeApi = vi.fn() + + expect(() => initServiceWorkerPrevention()).not.toThrow() + }) + + it("should not throw even if prevention fails", () => { + // Mock VS Code webview environment + ;(global as any).acquireVsCodeApi = vi.fn() + + // Mock console.error + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}) + + // Create a scenario where defineProperty might fail + // by stubbing a read-only navigator + const readOnlyNavigator = { + get serviceWorker() { + throw new Error("Cannot access serviceWorker") + }, + } + vi.stubGlobal("navigator", readOnlyNavigator) + + // Should not throw even if there's an error + expect(() => initServiceWorkerPrevention()).not.toThrow() + + consoleErrorSpy.mockRestore() + }) + + it("should successfully prevent ServiceWorker registration when called", async () => { + // Mock VS Code webview environment + ;(global as any).acquireVsCodeApi = vi.fn() + + // Mock console.warn to check for warning message + const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}) + + initServiceWorkerPrevention() + + // Try to register a service worker + await expect(navigator.serviceWorker.register("/sw.js")).rejects.toThrow() + + // Should have logged a warning + expect(consoleWarnSpy).toHaveBeenCalledWith( + "ServiceWorker registration blocked in VS Code webview environment", + ) + + consoleWarnSpy.mockRestore() + }) + }) +}) diff --git a/webview-ui/src/utils/serviceWorkerPrevention.ts b/webview-ui/src/utils/serviceWorkerPrevention.ts new file mode 100644 index 00000000000..ae4441f455c --- /dev/null +++ b/webview-ui/src/utils/serviceWorkerPrevention.ts @@ -0,0 +1,64 @@ +/** + * Prevents ServiceWorker registration in VS Code webview environment + * This is necessary because VS Code webviews have security restrictions + * that don't allow ServiceWorker registration, which can cause errors + * with third-party libraries like PostHog that attempt to use them. + */ + +/** + * Checks if we're running in a VS Code webview environment + */ +export function isVSCodeWebview(): boolean { + // Check for VS Code webview-specific global + return typeof acquireVsCodeApi !== "undefined" +} + +/** + * Disables ServiceWorker registration by overriding the navigator.serviceWorker API + * This prevents third-party libraries from attempting to register ServiceWorkers + * which would fail in the VS Code webview environment. + */ +export function disableServiceWorkerRegistration(): void { + if (!isVSCodeWebview()) { + // Only disable in VS Code webview environment + return + } + + // Override navigator.serviceWorker to prevent registration attempts + if (typeof navigator !== "undefined" && "serviceWorker" in navigator) { + // Create a mock serviceWorker object that prevents registration + const mockServiceWorker = { + register: () => { + console.warn("ServiceWorker registration blocked in VS Code webview environment") + return Promise.reject(new Error("ServiceWorker registration is not allowed in VS Code webview")) + }, + getRegistration: () => Promise.resolve(undefined), + getRegistrations: () => Promise.resolve([]), + ready: new Promise(() => { + // Never resolves, as no ServiceWorker will be ready + }), + controller: null, + oncontrollerchange: null, + onmessage: null, + onmessageerror: null, + } + + // Override the serviceWorker property + Object.defineProperty(navigator, "serviceWorker", { + get: () => mockServiceWorker, + configurable: true, + }) + } +} + +/** + * Initialize ServiceWorker prevention + * This should be called as early as possible in the application lifecycle + */ +export function initServiceWorkerPrevention(): void { + try { + disableServiceWorkerRegistration() + } catch (error) { + console.error("Failed to initialize ServiceWorker prevention:", error) + } +}