electrobun 1.18.4-beta.17 → 1.18.4-beta.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +14 -9
  2. package/dash.config.ts +3 -3
  3. package/dist/api/browser/global.d.ts +2 -0
  4. package/dist/api/browser/webviewtag.ts +14 -7
  5. package/dist/api/browser/wgputag.ts +1 -1
  6. package/dist/api/config/ElectrobunConfig.ts +11 -49
  7. package/dist/api/preload/.generated/compiled.ts +2 -2
  8. package/dist/api/preload/events.test.ts +20 -0
  9. package/dist/api/preload/events.ts +9 -0
  10. package/dist/api/preload/index-sandboxed.ts +2 -0
  11. package/dist/api/preload/index.ts +2 -6
  12. package/dist/api/sdks/{bun → main}/core/BuildConfig.ts +0 -2
  13. package/dist/api/shared/bridge-payload-ownership.test.ts +1 -1
  14. package/dist/api/shared/linux-tray-contract.test.ts +4 -4
  15. package/dist/api/shared/package-exports.test.ts +59 -31
  16. package/dist/api/shared/published-package-contract.test.ts +8 -3
  17. package/dist/api/shared/spell-check.test.js +2 -2
  18. package/dist/api/shared/window-visibility.test.ts +2 -2
  19. package/dist/api/shared/windows-unicode-ui.test.ts +1 -1
  20. package/dist/preload-full.js +6 -3
  21. package/dist/preload-sandboxed.js +6 -0
  22. package/package.json +37 -20
  23. /package/dist/api/sdks/{bun → main}/__tests__/build-config.test.ts +0 -0
  24. /package/dist/api/sdks/{bun → main}/__tests__/ffi-contract.test.ts +0 -0
  25. /package/dist/api/sdks/{bun → main}/core/ApplicationMenu.ts +0 -0
  26. /package/dist/api/sdks/{bun → main}/core/BrowserView.ts +0 -0
  27. /package/dist/api/sdks/{bun → main}/core/BrowserWindow.ts +0 -0
  28. /package/dist/api/sdks/{bun → main}/core/ContextMenu.ts +0 -0
  29. /package/dist/api/sdks/{bun → main}/core/GpuWindow.ts +0 -0
  30. /package/dist/api/sdks/{bun → main}/core/Paths.ts +0 -0
  31. /package/dist/api/sdks/{bun → main}/core/Socket.ts +0 -0
  32. /package/dist/api/sdks/{bun → main}/core/Tray.ts +0 -0
  33. /package/dist/api/sdks/{bun → main}/core/Updater.ts +0 -0
  34. /package/dist/api/sdks/{bun → main}/core/Utils.ts +0 -0
  35. /package/dist/api/sdks/{bun → main}/core/WGPUView.ts +0 -0
  36. /package/dist/api/sdks/{bun → main}/core/WindowsUpdateTask.test.ts +0 -0
  37. /package/dist/api/sdks/{bun → main}/core/WindowsUpdateTask.ts +0 -0
  38. /package/dist/api/sdks/{bun → main}/core/menuRoles.ts +0 -0
  39. /package/dist/api/sdks/{bun → main}/entries/app-menu.ts +0 -0
  40. /package/dist/api/sdks/{bun → main}/entries/browser-view.ts +0 -0
  41. /package/dist/api/sdks/{bun → main}/entries/browser-window.ts +0 -0
  42. /package/dist/api/sdks/{bun → main}/entries/build-config.ts +0 -0
  43. /package/dist/api/sdks/{bun → main}/entries/context-menu.ts +0 -0
  44. /package/dist/api/sdks/{bun → main}/entries/events.ts +0 -0
  45. /package/dist/api/sdks/{bun → main}/entries/gpu-window.ts +0 -0
  46. /package/dist/api/sdks/{bun → main}/entries/native.ts +0 -0
  47. /package/dist/api/sdks/{bun → main}/entries/paths.ts +0 -0
  48. /package/dist/api/sdks/{bun → main}/entries/rpc.ts +0 -0
  49. /package/dist/api/sdks/{bun → main}/entries/socket.ts +0 -0
  50. /package/dist/api/sdks/{bun → main}/entries/tray.ts +0 -0
  51. /package/dist/api/sdks/{bun → main}/entries/updater.ts +0 -0
  52. /package/dist/api/sdks/{bun → main}/entries/utils.ts +0 -0
  53. /package/dist/api/sdks/{bun → main}/entries/webgpu.ts +0 -0
  54. /package/dist/api/sdks/{bun → main}/entries/wgpu-view.ts +0 -0
  55. /package/dist/api/sdks/{bun → main}/events/ApplicationEvents.ts +0 -0
  56. /package/dist/api/sdks/{bun → main}/events/event.ts +0 -0
  57. /package/dist/api/sdks/{bun → main}/events/eventEmitter.ts +0 -0
  58. /package/dist/api/sdks/{bun → main}/events/trayEvents.ts +0 -0
  59. /package/dist/api/sdks/{bun → main}/events/webviewEvents.ts +0 -0
  60. /package/dist/api/sdks/{bun → main}/events/windowEvents.ts +0 -0
  61. /package/dist/api/sdks/{bun → main}/index.ts +0 -0
  62. /package/dist/api/sdks/{bun → main}/proc/eventBridge.test.ts +0 -0
  63. /package/dist/api/sdks/{bun → main}/proc/eventBridge.ts +0 -0
  64. /package/dist/api/sdks/{bun → main}/proc/linux.md +0 -0
  65. /package/dist/api/sdks/{bun → main}/proc/native.ts +0 -0
  66. /package/dist/api/sdks/{bun → main}/webGPU.ts +0 -0
  67. /package/dist/api/sdks/{bun → main}/webgpuAdapter.ts +0 -0
  68. /package/dist/api/sdks/{bun → main}/webgpuRenderPass.test.ts +0 -0
  69. /package/dist/api/sdks/{bun → main}/webgpuRenderPass.ts +0 -0
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <a href="https://electrobun.dev"><img src="https://github.com/blackboardsh/electrobun/assets/75102186/8799b522-0507-45e9-86e3-c3cfded1aa7c" alt="Logo" height=170></a>
2
+ <a href="https://framework.blackboard.sh/electrobun/"><img src="https://github.com/blackboardsh/electrobun/assets/75102186/8799b522-0507-45e9-86e3-c3cfded1aa7c" alt="Logo" height=170></a>
3
3
  </p>
4
4
 
5
5
  <h1 align="center">Electrobun</h1>
@@ -13,28 +13,33 @@
13
13
 
14
14
  ## What is Electrobun?
15
15
 
16
- Electrobun aims to be a complete **solution-in-a-box** for building, updating, and shipping ultra fast, tiny, and cross-platform desktop applications written in Typescript.
17
- Under the hood it uses Cottontail, Electrobun's JSC-based JavaScript runtime, to execute the main process and bundle webview TypeScript. Native bindings are written in ObjC and C++, with several core components written in <a href="https://ziglang.org/">Zig</a>.
16
+ Electrobun aims to be a complete **solution-in-a-box** for building, updating, and shipping fast, compact, cross-platform desktop applications written in TypeScript.
17
+ Hutch is the native build and package-management CLI. Cottontail is Electrobun's JSC-based default JavaScript runtime. Electrobun's platform layer combines Zig, Objective-C, and C++.
18
18
 
19
19
  Visit <a href="https://framework.blackboard.sh/electrobun/">https://framework.blackboard.sh/electrobun/</a> to see api documentation, guides, and more.
20
20
 
21
- You use it via npm.
21
+ Install Hutch globally, then use it to create a project, install the npm package that provides Electrobun's SDKs, and build the application:
22
+
23
+ ```bash
24
+ curl -fsSL https://hutch.blackboard.sh/hutch/install.sh | sh
25
+ hutch electrobun init
26
+ ```
22
27
 
23
28
  Don't miss our:
24
- - self-extracting bundles that use ZSTD compression for more compact distributables as small as 16MB
25
- - zig optimized BSDIFF implementation that lets you ship tiny app updates as small as 4KB
29
+ - self-extracting bundles that use Zstandard compression for compact distributables
30
+ - a Zig-optimized BSDIFF implementation that can produce kilobyte-scale updates
26
31
  - `bundleCEF` flag to bundle and pin Chromium for those that want that tradeoff of consistency over file size
27
32
  - `bundleWGPU` that lets you use Bun Typescript -> WGPU to control a native GPU surface without a webview
28
33
  - Our Three.js and Babylon.js adapters that work directly in the Cottontail main process
29
- - Our `<electrobun-webview>` and `<electrobun-wpgu>` html elements that let you composit proper OOPIFs and native GPU surfaces into your UIs
34
+ - Our `<electrobun-webview>` and `<electrobun-wgpu>` HTML elements that let you composite isolated webviews and native GPU surfaces into your UIs
30
35
  - so much more.
31
36
 
32
37
  **Project Goals**
33
38
 
34
39
  - Write typescript for the main process and webviews without having to think about it.
35
40
  - Isolation between main and webview processes with fast, typed, easy to implement RPC between them.
36
- - Small self-extracting app bundles ~14MB (when using the system webview, most of this is the Cottontail/JSC runtime)
37
- - Even smaller app updates as small as 4KB (using bsdiff it only downloads tiny patches between versions)
41
+ - Small self-extracting app bundles when using the system webview
42
+ - Small updates that use binary patches before falling back to a compressed full download
38
43
  - Provide everything you need in one tightly integrated workflow to start writing code in 5 minutes and distribute in 10.
39
44
 
40
45
  ## Apps Built with Electrobun
package/dash.config.ts CHANGED
@@ -1,7 +1,7 @@
1
- // @dash cli=0.5.0-canary.5 cottontail=0.2.3
1
+ // @dash cli=0.5.0-canary.8 cottontail=0.2.3
2
2
  export default {
3
3
  scripts: {
4
- start: "hutch src/sdks/bun/index.ts",
4
+ start: "hutch src/sdks/main/index.ts",
5
5
  "check-zig-version": "vendors/zig/zig version",
6
6
  "check-rust-version": "vendors/rust/bin/rustc --version",
7
7
  "build:dev": "hutch build.ts",
@@ -34,7 +34,7 @@ export default {
34
34
  "test:windows-ui-native": "hutch scripts/test-windows-ui-native.js",
35
35
  "test:windows-ui-native-integration":
36
36
  "hutch scripts/test-windows-ui-native.js --require-native-wrapper",
37
- "test:unit": "node scripts/run-cottontail-test.js src/shared src/sdks/bun src/config src/preload && hutch test:dialog-paths-native && hutch test:linux-dpi-native && hutch test:linux-x11-geometry-native && hutch test:views-url-native && hutch test:webview2-permissions && hutch test:windows-ui-native",
37
+ "test:unit": "node scripts/run-cottontail-test.js src/shared src/sdks/main src/config src/preload && hutch test:dialog-paths-native && hutch test:linux-dpi-native && hutch test:linux-x11-geometry-native && hutch test:views-url-native && hutch test:webview2-permissions && hutch test:windows-ui-native",
38
38
  "test:linux-native-dialog":
39
39
  "node scripts/run-cottontail-test.js src/shared/linux-native-file-dialog.test.ts && scripts/test-linux-native-file-dialog.sh",
40
40
  "test:cef-debug": "scripts/test-cef-remote-debugging.ts",
@@ -19,6 +19,7 @@ interface MessageHandler {
19
19
 
20
20
  declare global {
21
21
  interface Window {
22
+ __electrobunPlatform: "linux" | "macos" | "windows";
22
23
  __electrobunWebviewId: number;
23
24
  __electrobunWindowId: number;
24
25
  __electrobunRpcSocketPort: number;
@@ -31,6 +32,7 @@ declare global {
31
32
  __electrobunInternalBridge?: MessageHandler;
32
33
  __electrobunHostBridge?: MessageHandler;
33
34
  __electrobunBunBridge?: MessageHandler;
35
+ __electrobunSendToHost: (message: unknown) => void;
34
36
  }
35
37
  }
36
38
 
@@ -1,12 +1,20 @@
1
1
  import "./global.d.ts";
2
2
 
3
3
  type WebviewEventTypes =
4
+ | "will-navigate"
4
5
  | "did-navigate"
5
6
  | "did-navigate-in-page"
6
7
  | "did-commit-navigation"
7
8
  | "dom-ready"
8
9
  | "host-message"
9
- | "new-window-open";
10
+ | "new-window-open"
11
+ | "download-started"
12
+ | "download-progress"
13
+ | "download-completed"
14
+ | "download-failed"
15
+ | "load-started"
16
+ | "load-committed"
17
+ | "load-finished";
10
18
 
11
19
  /**
12
20
  * Interface representing an <electrobun-webview> custom element.
@@ -19,14 +27,13 @@ type WebviewEventTypes =
19
27
  */
20
28
  interface WebviewTagElement extends HTMLElement {
21
29
  // Properties
22
- webviewId?: number;
30
+ webviewId: number | null;
23
31
  maskSelectors: Set<string>;
24
32
  transparent: boolean;
25
33
  passthroughEnabled: boolean;
26
34
  spellCheckEnabled: boolean;
27
35
  hidden: boolean;
28
- hiddenMirrorMode: boolean;
29
- partition: string | null;
36
+ readonly sandbox: boolean;
30
37
 
31
38
  // Attribute-backed properties (getters/setters)
32
39
  src: string | null;
@@ -49,9 +56,9 @@ interface WebviewTagElement extends HTMLElement {
49
56
  setSpellCheck(enabled: boolean): Promise<boolean>;
50
57
 
51
58
  // Visibility and interaction
52
- toggleTransparent(transparent?: boolean, bypassState?: boolean): void;
53
- togglePassthrough(enablePassthrough?: boolean, bypassState?: boolean): void;
54
- toggleHidden(hidden?: boolean, bypassState?: boolean): void;
59
+ toggleTransparent(transparent?: boolean): void;
60
+ togglePassthrough(enablePassthrough?: boolean): void;
61
+ toggleHidden(hidden?: boolean): void;
55
62
 
56
63
  // Events - listener receives a CustomEvent with detail property
57
64
  on(event: WebviewEventTypes, listener: (event: CustomEvent) => void): void;
@@ -12,7 +12,7 @@ type WgpuEventTypes = "ready";
12
12
  */
13
13
  interface WgpuTagElement extends HTMLElement {
14
14
  // Properties
15
- wgpuViewId?: number;
15
+ wgpuViewId: number | null;
16
16
  transparent: boolean;
17
17
  passthroughEnabled: boolean;
18
18
  hidden: boolean;
@@ -8,12 +8,14 @@ import type { WindowsWebView2Permission } from "./windowsPermissions";
8
8
  type BundlerOptions = {
9
9
  external?: string[];
10
10
  define?: Record<string, string>;
11
- loader?: Record<string, string>;
11
+ alias?: Record<string, string>;
12
12
  minify?: boolean;
13
13
  sourcemap?: boolean | "inline" | "external" | "linked";
14
- splitting?: boolean;
15
- plugins?: unknown[];
16
- [key: string]: unknown;
14
+ target?: string | string[];
15
+ };
16
+
17
+ type ViewBundlerOptions = BundlerOptions & {
18
+ format?: "esm" | "cjs" | "iife";
17
19
  };
18
20
 
19
21
  type CarrotFileActivatorConfig = {
@@ -228,8 +230,8 @@ export interface ElectrobunConfig {
228
230
 
229
231
  /**
230
232
  * Browser view build configurations.
231
- * Each view accepts bundler options (plugins, sourcemap, minify, define, etc.)
232
- * in addition to the entrypoint.
233
+ * Each view accepts the supported bundler options in addition to the
234
+ * entrypoint.
233
235
  */
234
236
  views?: {
235
237
  [viewName: string]: {
@@ -237,7 +239,7 @@ export interface ElectrobunConfig {
237
239
  * Entry point for this view's TypeScript code
238
240
  */
239
241
  entrypoint: string;
240
- } & BundlerOptions;
242
+ } & ViewBundlerOptions;
241
243
  };
242
244
 
243
245
  /**
@@ -259,47 +261,6 @@ export interface ElectrobunConfig {
259
261
  */
260
262
  artifactFolder?: string;
261
263
 
262
- /**
263
- * Build targets to compile for
264
- * Can be "current", "all", or comma-separated list like "macos-arm64,win-x64"
265
- */
266
- targets?: string;
267
-
268
- /**
269
- * Enable ASAR archive packaging for bundled assets
270
- * When enabled, all files in the Resources folder will be packed into an app.asar archive
271
- * @default false
272
- */
273
- useAsar?: boolean;
274
-
275
- /**
276
- * Glob patterns for files to exclude from ASAR packing (extract to app.asar.unpacked)
277
- * Useful for native modules or executables that need to be accessible as regular files
278
- * @default ["*.node", "*.dll", "*.dylib", "*.so"]
279
- */
280
- asarUnpack?: string[];
281
-
282
- /**
283
- * Override the CEF (Chromium Embedded Framework) version.
284
- * Format: "CEF_VERSION+chromium-CHROMIUM_VERSION"
285
- * Example: "144.0.11+ge135be2+chromium-144.0.7559.97"
286
- *
287
- * Check the electrobun-cef-compat compatibility matrix for tested combinations
288
- * before overriding. Using an untested version may cause runtime issues.
289
- * @default Uses the version bundled with this Electrobun release
290
- */
291
- cefVersion?: string;
292
-
293
- /**
294
- * Override the Dawn (WebGPU) version.
295
- * Format: semver string (e.g., "0.2.3") or tag (e.g., "v0.2.3-beta.0")
296
- *
297
- * This downloads the specified electrobun-dawn release and uses it
298
- * instead of the latest release.
299
- * @default Uses the latest electrobun-dawn release
300
- */
301
- wgpuVersion?: string;
302
-
303
264
  /**
304
265
  * Additional file or directory paths to watch for changes during `electrobun dev --watch`.
305
266
  * Paths are relative to the project root.
@@ -468,7 +429,8 @@ export interface ElectrobunConfig {
468
429
  autoGrantPermissions?: WindowsWebView2Permission[];
469
430
 
470
431
  /**
471
- * Path to application icon (.ico format)
432
+ * Path to an application icon in ICO or PNG format. PNG input is
433
+ * converted to ICO by Hutch.
472
434
  * Used for the installer/extractor wrapper, desktop shortcuts, and taskbar
473
435
  * Should include multiple sizes (16x16, 32x32, 48x48, 256x256) for best results
474
436
  * @example "assets/icon.ico"
@@ -2,7 +2,7 @@
2
2
  // Run "hutch build.ts" or "hutch run build:dev" from the package folder to regenerate.
3
3
 
4
4
  // Full preload for trusted webviews (RPC, encryption, drag regions, webview tags)
5
- export const preloadScript = "(function(){// src/preload/encryption.ts\nfunction base64ToUint8Array(base64) {\n return new Uint8Array(atob(base64).split(\"\").map((char) => char.charCodeAt(0)));\n}\nfunction uint8ArrayToBase64(uint8Array) {\n let binary = \"\";\n for (let i = 0;i < uint8Array.length; i++) {\n binary += String.fromCharCode(uint8Array[i]);\n }\n return btoa(binary);\n}\nfunction toArrayBuffer(bytes) {\n const buffer = new ArrayBuffer(bytes.byteLength);\n new Uint8Array(buffer).set(bytes);\n return buffer;\n}\nasync function generateKeyFromBytes(rawKey) {\n return await window.crypto.subtle.importKey(\"raw\", toArrayBuffer(rawKey), { name: \"AES-GCM\" }, true, [\"encrypt\", \"decrypt\"]);\n}\nasync function initEncryption() {\n const secretKey = await generateKeyFromBytes(new Uint8Array(window.__electrobunSecretKeyBytes));\n const encryptString = async (plaintext) => {\n const encoder = new TextEncoder;\n const encodedText = encoder.encode(plaintext);\n const iv = window.crypto.getRandomValues(new Uint8Array(12));\n const encryptedBuffer = await window.crypto.subtle.encrypt({ name: \"AES-GCM\", iv: toArrayBuffer(iv) }, secretKey, toArrayBuffer(encodedText));\n const encryptedData = new Uint8Array(encryptedBuffer.slice(0, -16));\n const tag = new Uint8Array(encryptedBuffer.slice(-16));\n return {\n encryptedData: uint8ArrayToBase64(encryptedData),\n iv: uint8ArrayToBase64(iv),\n tag: uint8ArrayToBase64(tag)\n };\n };\n const decryptString = async (encryptedDataB64, ivB64, tagB64) => {\n const encryptedData = base64ToUint8Array(encryptedDataB64);\n const iv = base64ToUint8Array(ivB64);\n const tag = base64ToUint8Array(tagB64);\n const combinedData = new Uint8Array(encryptedData.length + tag.length);\n combinedData.set(encryptedData);\n combinedData.set(tag, encryptedData.length);\n const decryptedBuffer = await window.crypto.subtle.decrypt({ name: \"AES-GCM\", iv: toArrayBuffer(iv) }, secretKey, toArrayBuffer(combinedData));\n const decoder = new TextDecoder;\n return decoder.decode(decryptedBuffer);\n };\n window.__electrobun_encrypt = encryptString;\n window.__electrobun_decrypt = decryptString;\n}\n\n// src/preload/internalRpc.ts\nvar pendingRequests = {};\nvar requestId = 0;\nvar isProcessingQueue = false;\nvar sendQueue = [];\nfunction processQueue() {\n if (isProcessingQueue) {\n setTimeout(processQueue);\n return;\n }\n if (sendQueue.length === 0)\n return;\n isProcessingQueue = true;\n const batch = JSON.stringify(sendQueue);\n sendQueue.length = 0;\n window.__electrobunInternalBridge?.postMessage(batch);\n setTimeout(() => {\n isProcessingQueue = false;\n }, 2);\n}\nfunction send(type, payload) {\n sendQueue.push(JSON.stringify({ type: \"message\", id: type, payload }));\n processQueue();\n}\nfunction request(type, payload) {\n return new Promise((resolve, reject) => {\n const id = `req_${++requestId}_${Date.now()}`;\n pendingRequests[id] = { resolve, reject };\n sendQueue.push(JSON.stringify({\n type: \"request\",\n method: type,\n id,\n params: payload,\n hostWebviewId: window.__electrobunWebviewId\n }));\n processQueue();\n setTimeout(() => {\n if (pendingRequests[id]) {\n delete pendingRequests[id];\n reject(new Error(`Request timeout: ${type}`));\n }\n }, 1e4);\n });\n}\nfunction handleResponse(msg) {\n if (msg && msg.type === \"response\" && msg.id) {\n const pending = pendingRequests[msg.id];\n if (pending) {\n delete pendingRequests[msg.id];\n if (msg.success)\n pending.resolve(msg.payload);\n else\n pending.reject(msg.payload);\n }\n }\n}\n\n// src/preload/dragRegions.ts\nvar DRAG_CLASS = \"electrobun-webkit-app-region-drag\";\nvar NO_DRAG_CLASS = \"electrobun-webkit-app-region-no-drag\";\nvar MIRRORED_PROPERTY = \"--electrobun-app-region\";\nvar MIRROR_ATTRIBUTE = \"data-electrobun-app-region-mirror\";\nvar APP_REGION_PROPERTIES = [\n MIRRORED_PROPERTY,\n \"-webkit-app-region\",\n \"app-region\",\n \"window-drag\"\n];\nvar DRAG_REGION_COMPATIBILITY_CSS = `\n.electrobun-webkit-app-region-drag {\n\t-webkit-app-region: drag;\n\tapp-region: drag;\n}\n.electrobun-webkit-app-region-no-drag {\n\t-webkit-app-region: no-drag;\n\tapp-region: no-drag;\n}\n`;\nvar processedSourceSignatures = new WeakMap;\nvar stylesheetMirrors = new Map;\nvar pendingLinkSignatures = new WeakMap;\nvar stylesheetMirroringInitialized = false;\nfunction normalizedRegion(value) {\n const normalized = value?.trim().toLowerCase();\n if (normalized === \"drag\" || normalized === \"no-drag\")\n return normalized;\n return null;\n}\nfunction previousSignificantCharacter(source, offset) {\n let index = offset - 1;\n while (index >= 0) {\n while (index >= 0 && /\\s/.test(source[index] ?? \"\"))\n index--;\n if (index >= 1 && source[index] === \"/\" && source[index - 1] === \"*\") {\n const commentStart = source.lastIndexOf(\"/*\", index - 1);\n if (commentStart < 0)\n return \"\";\n index = commentStart - 1;\n continue;\n }\n return source[index] ?? \"\";\n }\n return \"\";\n}\nfunction isIdentifierCharacter(character) {\n return !!character && /[a-zA-Z0-9_-]/.test(character);\n}\nfunction rewriteAppRegionDeclarations(source) {\n const propertyNames = [\"-webkit-app-region\", \"window-drag\", \"app-region\"];\n let output = \"\";\n let cursor = 0;\n let index = 0;\n while (index < source.length) {\n if (source[index] === \"/\" && source[index + 1] === \"*\") {\n const commentEnd = source.indexOf(\"*/\", index + 2);\n index = commentEnd < 0 ? source.length : commentEnd + 2;\n continue;\n }\n if (source[index] === '\"' || source[index] === \"'\") {\n const quote = source[index];\n index++;\n while (index < source.length) {\n if (source[index] === \"\\\\\") {\n index += 2;\n continue;\n }\n if (source[index] === quote) {\n index++;\n break;\n }\n index++;\n }\n continue;\n }\n let matchedProperty;\n for (const propertyName of propertyNames) {\n if (source.slice(index, index + propertyName.length).toLowerCase() === propertyName && !isIdentifierCharacter(source[index - 1]) && !isIdentifierCharacter(source[index + propertyName.length])) {\n matchedProperty = propertyName;\n break;\n }\n }\n if (matchedProperty) {\n let colonOffset = index + matchedProperty.length;\n while (/\\s/.test(source[colonOffset] ?? \"\"))\n colonOffset++;\n const previous = previousSignificantCharacter(source, index);\n if (source[colonOffset] === \":\" && (previous === \"{\" || previous === \";\")) {\n output += source.slice(cursor, index) + MIRRORED_PROPERTY;\n index += matchedProperty.length;\n cursor = index;\n continue;\n }\n }\n index++;\n }\n return output + source.slice(cursor);\n}\nfunction nestedRuleContainer(rule) {\n const candidate = rule;\n if (candidate.cssRules && typeof candidate.deleteRule === \"function\") {\n return candidate;\n }\n return null;\n}\nfunction pruneToAppRegionRules(container) {\n let hasAppRegionRule = false;\n for (let index = container.cssRules.length - 1;index >= 0; index--) {\n const rule = container.cssRules[index];\n if (!rule)\n continue;\n const style = rule.style;\n let hasAppRegionDeclaration = false;\n if (style && typeof style.getPropertyValue === \"function\") {\n for (let propertyIndex = style.length - 1;propertyIndex >= 0; propertyIndex--) {\n const propertyName = style.item(propertyIndex);\n if (propertyName === MIRRORED_PROPERTY) {\n hasAppRegionDeclaration = true;\n } else {\n style.removeProperty(propertyName);\n }\n }\n }\n const nested = nestedRuleContainer(rule);\n const hasNestedAppRegionRule = nested ? pruneToAppRegionRules(nested) : false;\n if (!hasAppRegionDeclaration && !hasNestedAppRegionRule) {\n container.deleteRule(index);\n continue;\n }\n hasAppRegionRule = true;\n }\n return hasAppRegionRule;\n}\nfunction serializeRules(rules) {\n let cssText = \"\";\n for (let index = 0;index < rules.length; index++) {\n const rule = rules[index];\n if (rule)\n cssText += `${rule.cssText}\n`;\n }\n return cssText;\n}\nfunction removeStylesheetMirror(source) {\n stylesheetMirrors.get(source)?.remove();\n stylesheetMirrors.delete(source);\n}\nfunction installStylesheetMirror(source, cssText, signature) {\n if (processedSourceSignatures.get(source) === signature)\n return;\n processedSourceSignatures.set(source, signature);\n removeStylesheetMirror(source);\n const mirroredCss = rewriteAppRegionDeclarations(cssText);\n if (mirroredCss === cssText || !source.parentNode)\n return;\n const mirror = source.ownerDocument.createElement(\"style\");\n mirror.setAttribute(MIRROR_ATTRIBUTE, \"\");\n mirror.media = \"not all\";\n if (source.nonce)\n mirror.nonce = source.nonce;\n mirror.textContent = mirroredCss;\n source.parentNode.insertBefore(mirror, source.nextSibling);\n let sheet = mirror.sheet;\n if (!sheet || !pruneToAppRegionRules(sheet)) {\n mirror.remove();\n return;\n }\n mirror.textContent = serializeRules(sheet.cssRules);\n sheet = mirror.sheet;\n if (!sheet) {\n mirror.remove();\n return;\n }\n mirror.media = source.media;\n if (source.sheet?.disabled)\n sheet.disabled = true;\n stylesheetMirrors.set(source, mirror);\n}\nfunction isMirroredStyle(element) {\n return element.tagName === \"STYLE\" && element.hasAttribute(MIRROR_ATTRIBUTE);\n}\nfunction isStylesheetSource(element) {\n if (element.tagName === \"STYLE\")\n return !isMirroredStyle(element);\n return element.tagName === \"LINK\" && element.relList.contains(\"stylesheet\");\n}\nfunction nodeContainsStylesheetSource(node) {\n const element = node;\n if (typeof element.matches !== \"function\")\n return false;\n return isStylesheetSource(element) || !!element.querySelector(\"style:not([data-electrobun-app-region-mirror]), link[rel~='stylesheet']\");\n}\nfunction mutationsAffectStylesheets(mutations) {\n for (const mutation of mutations) {\n if (mutation.type === \"attributes\") {\n const element = mutation.target;\n if (element.tagName === \"LINK\" || isStylesheetSource(element))\n return true;\n continue;\n }\n if (mutation.type === \"characterData\") {\n const parent = mutation.target.parentElement;\n if (parent && isStylesheetSource(parent))\n return true;\n continue;\n }\n const target = mutation.target;\n if (typeof target.matches === \"function\" && isStylesheetSource(target)) {\n return true;\n }\n for (let index = 0;index < mutation.addedNodes.length; index++) {\n const node = mutation.addedNodes[index];\n if (node && nodeContainsStylesheetSource(node))\n return true;\n }\n for (let index = 0;index < mutation.removedNodes.length; index++) {\n const node = mutation.removedNodes[index];\n if (node && nodeContainsStylesheetSource(node))\n return true;\n }\n }\n return false;\n}\nfunction processStyleElement(style) {\n if (isMirroredStyle(style))\n return;\n const cssText = style.textContent ?? \"\";\n installStylesheetMirror(style, cssText, `${style.media}\n${cssText}`);\n}\nfunction processLinkElement(link) {\n if (!link.relList.contains(\"stylesheet\") || !link.href) {\n removeStylesheetMirror(link);\n return;\n }\n const signature = `${link.media}\n${link.href}`;\n if (processedSourceSignatures.get(link) === signature || pendingLinkSignatures.get(link) === signature) {\n return;\n }\n pendingLinkSignatures.set(link, signature);\n fetch(link.href, { credentials: \"same-origin\" }).then((response) => {\n if (!response.ok)\n throw new Error(`HTTP ${response.status}`);\n return response.text();\n }).then((cssText) => {\n if (link.isConnected && `${link.media}\n${link.href}` === signature) {\n installStylesheetMirror(link, cssText, signature);\n }\n }).catch(() => {}).finally(() => {\n if (pendingLinkSignatures.get(link) === signature) {\n pendingLinkSignatures.delete(link);\n }\n });\n}\nfunction scanStylesheetSources() {\n document.querySelectorAll(\"style, link[rel~='stylesheet']\").forEach((element) => {\n if (element.tagName === \"STYLE\") {\n processStyleElement(element);\n } else {\n processLinkElement(element);\n }\n });\n for (const [source, mirror] of stylesheetMirrors) {\n if (!source.isConnected) {\n mirror.remove();\n stylesheetMirrors.delete(source);\n }\n }\n}\nfunction initStylesheetMirroring() {\n if (stylesheetMirroringInitialized)\n return;\n stylesheetMirroringInitialized = true;\n let scanScheduled = false;\n const scheduleScan = () => {\n if (scanScheduled)\n return;\n scanScheduled = true;\n queueMicrotask(() => {\n scanScheduled = false;\n scanStylesheetSources();\n });\n };\n new MutationObserver((mutations) => {\n if (mutationsAffectStylesheets(mutations))\n scheduleScan();\n }).observe(document, {\n attributes: true,\n attributeFilter: [\"href\", \"media\", \"rel\"],\n characterData: true,\n childList: true,\n subtree: true\n });\n document.addEventListener(\"DOMContentLoaded\", scheduleScan, { once: true });\n scheduleScan();\n}\nfunction installCompatibilityStyles() {\n if (document.querySelector(\"style[data-electrobun-drag-region-compat]\"))\n return;\n const style = document.createElement(\"style\");\n style.setAttribute(\"data-electrobun-drag-region-compat\", \"\");\n style.textContent = DRAG_REGION_COMPATIBILITY_CSS;\n const install = () => {\n if (!document.head || style.isConnected)\n return;\n document.head.insertBefore(style, document.head.firstChild);\n };\n if (document.head)\n install();\n else\n document.addEventListener(\"DOMContentLoaded\", install, { once: true });\n}\nfunction inlineRegion(element) {\n const style = element.getAttribute?.(\"style\");\n if (!style)\n return null;\n const match = style.match(/(?:^|;)\\s*(?:-webkit-app-region|app-region|window-drag)\\s*:\\s*(no-drag|drag)\\b/i);\n return normalizedRegion(match?.[1]);\n}\nfunction computedRegion(element, readComputedStyle) {\n try {\n const style = readComputedStyle(element);\n for (const propertyName of APP_REGION_PROPERTIES) {\n const region = normalizedRegion(style.getPropertyValue(propertyName));\n if (region)\n return region;\n }\n } catch {}\n return null;\n}\nfunction eventTargetElement(target) {\n if (!target || typeof target.getAttribute !== \"function\") {\n return target?.parentElement ?? null;\n }\n return target;\n}\nfunction isAppRegionDragTarget(target, readComputedStyle = (element) => window.getComputedStyle(element)) {\n let element = eventTargetElement(target);\n let foundDragRegion = false;\n while (element) {\n if (element.classList?.contains(NO_DRAG_CLASS))\n return false;\n if (element.classList?.contains(DRAG_CLASS))\n foundDragRegion = true;\n const region = inlineRegion(element) ?? computedRegion(element, readComputedStyle);\n if (region === \"no-drag\")\n return false;\n if (region === \"drag\")\n foundDragRegion = true;\n element = element.parentElement;\n }\n return foundDragRegion;\n}\nfunction registerDragRegionListeners(targetDocument, getWindowId, sendMessage = send, readComputedStyle = (element) => window.getComputedStyle(element)) {\n targetDocument.addEventListener(\"mousedown\", (event) => {\n if (isAppRegionDragTarget(event.target, readComputedStyle)) {\n sendMessage(\"startWindowMove\", { id: getWindowId() });\n }\n });\n targetDocument.addEventListener(\"mouseup\", (event) => {\n if (isAppRegionDragTarget(event.target, readComputedStyle)) {\n sendMessage(\"stopWindowMove\", { id: getWindowId() });\n }\n });\n}\nfunction initDragRegions() {\n installCompatibilityStyles();\n initStylesheetMirroring();\n registerDragRegionListeners(document, () => window.__electrobunWindowId);\n}\n\n// src/preload/externalDropFocus.ts\nfunction initExternalDropFocusRestoration(targetWindow = window, platform = window.__electrobunPlatform, requestFocus = request, schedule = (callback) => setTimeout(callback)) {\n if (platform !== \"windows\")\n return;\n targetWindow.addEventListener(\"drop\", (event) => {\n const types = Array.from(event.dataTransfer?.types ?? []);\n if (!types.includes(\"Files\"))\n return;\n const previouslyFocusedElement = targetWindow.document.activeElement;\n schedule(() => {\n requestFocus(\"restoreWindowFocusAfterExternalDrop\", {\n windowId: targetWindow.__electrobunWindowId\n }).catch(() => {\n return;\n }).then(() => {\n targetWindow.focus();\n if (previouslyFocusedElement && \"focus\" in previouslyFocusedElement && typeof previouslyFocusedElement.focus === \"function\") {\n previouslyFocusedElement.focus();\n }\n });\n });\n }, true);\n}\n\n// src/preload/overlaySync.ts\nclass OverlaySyncController {\n element;\n options;\n lastRect = { x: 0, y: 0, width: 0, height: 0 };\n resizeObserver = null;\n positionLoop = null;\n resizeHandler = null;\n burstUntil = 0;\n constructor(element, options) {\n this.element = element;\n this.options = {\n onSync: options.onSync,\n getMasks: options.getMasks ?? (() => []),\n burstIntervalMs: options.burstIntervalMs ?? 50,\n baseIntervalMs: options.baseIntervalMs ?? 100,\n burstDurationMs: options.burstDurationMs ?? 500\n };\n }\n start() {\n this.resizeObserver = new ResizeObserver(() => this.sync());\n this.resizeObserver.observe(this.element);\n const loop = () => {\n this.sync();\n const now = performance.now();\n const interval = now < this.burstUntil ? this.options.burstIntervalMs : this.options.baseIntervalMs;\n this.positionLoop = setTimeout(loop, interval);\n };\n this.positionLoop = setTimeout(loop, this.options.baseIntervalMs);\n this.resizeHandler = () => this.sync(true);\n window.addEventListener(\"resize\", this.resizeHandler);\n }\n stop() {\n if (this.resizeObserver)\n this.resizeObserver.disconnect();\n if (this.positionLoop)\n clearTimeout(this.positionLoop);\n if (this.resizeHandler) {\n window.removeEventListener(\"resize\", this.resizeHandler);\n }\n this.resizeObserver = null;\n this.positionLoop = null;\n this.resizeHandler = null;\n }\n forceSync() {\n this.sync(true);\n }\n setLastRect(rect) {\n this.lastRect = rect;\n }\n sync(force = false) {\n const rect = this.element.getBoundingClientRect();\n const newRect = {\n x: rect.x,\n y: rect.y,\n width: rect.width,\n height: rect.height\n };\n if (newRect.width === 0 && newRect.height === 0) {\n return;\n }\n if (!force && newRect.x === this.lastRect.x && newRect.y === this.lastRect.y && newRect.width === this.lastRect.width && newRect.height === this.lastRect.height) {\n return;\n }\n this.burstUntil = performance.now() + this.options.burstDurationMs;\n this.lastRect = newRect;\n const masks = this.options.getMasks();\n this.options.onSync(newRect, JSON.stringify(masks));\n }\n}\n\n// src/preload/webviewTagNavigation.ts\nclass WebviewTagNavigationQueue {\n pending = null;\n beginInitialization() {\n this.pending = null;\n }\n defer(navigation) {\n this.pending = navigation;\n }\n take() {\n const navigation = this.pending;\n this.pending = null;\n return navigation;\n }\n}\n\n// src/preload/webviewTag.ts\nvar webviewRegistry = {};\n\nclass ElectrobunWebviewTag extends HTMLElement {\n webviewId = null;\n maskSelectors = new Set;\n _sync = null;\n _navigationQueue = new WebviewTagNavigationQueue;\n _initializationStarted = false;\n transparent = false;\n passthroughEnabled = false;\n spellCheckEnabled = false;\n hidden = false;\n sandboxed = false;\n _eventListeners = {};\n static get observedAttributes() {\n return [\"src\", \"html\", \"spellcheck\"];\n }\n constructor() {\n super();\n }\n connectedCallback() {\n requestAnimationFrame(() => this.initWebview());\n }\n attributeChangedCallback(name, oldValue, newValue) {\n if (oldValue === newValue)\n return;\n if (name === \"src\" && newValue !== null) {\n this.navigate({ kind: \"url\", value: newValue });\n } else if (name === \"html\" && newValue !== null) {\n this.navigate({ kind: \"html\", value: newValue });\n } else if (name === \"spellcheck\" && this.webviewId !== null) {\n this.setSpellCheck(newValue !== null && newValue.toLowerCase() !== \"false\");\n }\n }\n disconnectedCallback() {\n if (this.webviewId !== null) {\n send(\"webviewTagRemove\", { id: this.webviewId });\n delete webviewRegistry[this.webviewId];\n }\n if (this._sync)\n this._sync.stop();\n }\n getInitialNavigationRules() {\n const rawRules = this.getAttribute(\"navigation-rules\");\n if (rawRules === null) {\n return null;\n }\n const trimmed = rawRules.trim();\n if (!trimmed) {\n return [];\n }\n try {\n const parsed = JSON.parse(trimmed);\n if (!Array.isArray(parsed) || !parsed.every((rule) => typeof rule === \"string\")) {\n throw new Error(\"navigation-rules must be a JSON string array\");\n }\n return parsed;\n } catch (error) {\n console.error(\"Invalid navigation-rules attribute:\", error);\n return [];\n }\n }\n async initWebview() {\n this._initializationStarted = true;\n this._navigationQueue.beginInitialization();\n const rect = this.getBoundingClientRect();\n const initialRect = {\n x: rect.x,\n y: rect.y,\n width: rect.width,\n height: rect.height\n };\n const url = this.getAttribute(\"src\");\n const html = this.getAttribute(\"html\");\n const preload = this.getAttribute(\"preload\");\n const partition = this.getAttribute(\"partition\");\n const renderer = this.getAttribute(\"renderer\") || \"native\";\n const masks = this.getAttribute(\"masks\");\n const navigationRules = this.getInitialNavigationRules();\n const sandbox = this.hasAttribute(\"sandbox\");\n this.sandboxed = sandbox;\n const transparent = this.hasAttribute(\"transparent\");\n const passthrough = this.hasAttribute(\"passthrough\");\n const spellCheckAttribute = this.getAttribute(\"spellcheck\");\n const spellCheck = spellCheckAttribute !== null && spellCheckAttribute.toLowerCase() !== \"false\";\n this.transparent = transparent;\n this.passthroughEnabled = passthrough;\n this.spellCheckEnabled = spellCheck;\n if (transparent)\n this.style.opacity = \"0\";\n if (passthrough)\n this.style.pointerEvents = \"none\";\n if (masks) {\n masks.split(\",\").forEach((s) => this.maskSelectors.add(s.trim()));\n }\n try {\n const webviewInitParams = {\n hostWebviewId: window.__electrobunWebviewId,\n windowId: window.__electrobunWindowId,\n renderer,\n url,\n html,\n preload,\n partition,\n frame: initialRect,\n sandbox,\n transparent,\n passthrough,\n spellCheck,\n ...navigationRules === null ? {} : { navigationRules }\n };\n const webviewId = await request(\"webviewTagInit\", webviewInitParams);\n this.webviewId = webviewId;\n this.id = `electrobun-webview-${webviewId}`;\n webviewRegistry[webviewId] = this;\n const pendingNavigation = this._navigationQueue.take();\n if (pendingNavigation)\n this.navigate(pendingNavigation);\n this.setupObservers(initialRect);\n this.syncDimensions(true);\n requestAnimationFrame(() => {\n Object.values(webviewRegistry).forEach((webview) => {\n if (webview !== this && webview.webviewId !== null) {\n webview.syncDimensions(true);\n }\n });\n });\n } catch (err) {\n console.error(\"Failed to init webview:\", err);\n }\n }\n setupObservers(initialRect) {\n const getMasks = () => {\n const rect = this.getBoundingClientRect();\n const masks = [];\n this.maskSelectors.forEach((selector) => {\n try {\n document.querySelectorAll(selector).forEach((el) => {\n const mr = el.getBoundingClientRect();\n masks.push({\n x: mr.x - rect.x,\n y: mr.y - rect.y,\n width: mr.width,\n height: mr.height\n });\n });\n } catch (_e) {}\n });\n return masks;\n };\n this._sync = new OverlaySyncController(this, {\n onSync: (rect, masksJson) => {\n if (this.webviewId === null)\n return;\n send(\"webviewTagResize\", {\n id: this.webviewId,\n frame: rect,\n masks: masksJson\n });\n },\n getMasks,\n burstIntervalMs: 10,\n baseIntervalMs: 100,\n burstDurationMs: 50\n });\n this._sync.setLastRect(initialRect);\n this._sync.start();\n }\n syncDimensions(force = false) {\n if (!this._sync)\n return;\n if (force) {\n this._sync.forceSync();\n }\n }\n navigate(navigation) {\n if (this.webviewId === null) {\n this._navigationQueue.defer(navigation);\n return;\n }\n if (navigation.kind === \"url\") {\n send(\"webviewTagUpdateSrc\", {\n id: this.webviewId,\n url: navigation.value\n });\n } else {\n send(\"webviewTagUpdateHtml\", {\n id: this.webviewId,\n html: navigation.value\n });\n }\n }\n loadURL(url) {\n if (this.getAttribute(\"src\") === url) {\n this.navigate({ kind: \"url\", value: url });\n } else {\n this.setAttribute(\"src\", url);\n }\n }\n loadHTML(html) {\n if (this.webviewId === null && !this._initializationStarted) {\n this.setAttribute(\"html\", html);\n return;\n }\n this.navigate({ kind: \"html\", value: html });\n }\n reload() {\n if (this.webviewId !== null)\n send(\"webviewTagReload\", { id: this.webviewId });\n }\n goBack() {\n if (this.webviewId !== null)\n send(\"webviewTagGoBack\", { id: this.webviewId });\n }\n goForward() {\n if (this.webviewId !== null)\n send(\"webviewTagGoForward\", { id: this.webviewId });\n }\n async canGoBack() {\n if (this.webviewId === null)\n return false;\n return await request(\"webviewTagCanGoBack\", {\n id: this.webviewId\n });\n }\n async canGoForward() {\n if (this.webviewId === null)\n return false;\n return await request(\"webviewTagCanGoForward\", {\n id: this.webviewId\n });\n }\n async setSpellCheck(enabled) {\n this.spellCheckEnabled = enabled;\n if (this.webviewId === null)\n return false;\n return await request(\"webviewTagSetSpellCheck\", {\n id: this.webviewId,\n enabled\n });\n }\n toggleTransparent(value) {\n if (this.webviewId === null)\n return;\n this.transparent = value !== undefined ? value : !this.transparent;\n this.style.opacity = this.transparent ? \"0\" : \"\";\n send(\"webviewTagSetTransparent\", {\n id: this.webviewId,\n transparent: this.transparent\n });\n }\n togglePassthrough(value) {\n if (this.webviewId === null)\n return;\n this.passthroughEnabled = value !== undefined ? value : !this.passthroughEnabled;\n this.style.pointerEvents = this.passthroughEnabled ? \"none\" : \"\";\n send(\"webviewTagSetPassthrough\", {\n id: this.webviewId,\n enablePassthrough: this.passthroughEnabled\n });\n }\n toggleHidden(value) {\n if (this.webviewId === null)\n return;\n this.hidden = value !== undefined ? value : !this.hidden;\n send(\"webviewTagSetHidden\", { id: this.webviewId, hidden: this.hidden });\n }\n addMaskSelector(selector) {\n this.maskSelectors.add(selector);\n this.syncDimensions(true);\n }\n removeMaskSelector(selector) {\n this.maskSelectors.delete(selector);\n this.syncDimensions(true);\n }\n setNavigationRules(rules) {\n if (this.webviewId !== null) {\n send(\"webviewTagSetNavigationRules\", { id: this.webviewId, rules });\n }\n }\n findInPage(searchText, options) {\n if (this.webviewId === null)\n return;\n const forward = options?.forward !== false;\n const matchCase = options?.matchCase || false;\n send(\"webviewTagFindInPage\", {\n id: this.webviewId,\n searchText,\n forward,\n matchCase\n });\n }\n stopFindInPage() {\n if (this.webviewId !== null)\n send(\"webviewTagStopFind\", { id: this.webviewId });\n }\n openDevTools() {\n if (this.webviewId !== null)\n send(\"webviewTagOpenDevTools\", { id: this.webviewId });\n }\n closeDevTools() {\n if (this.webviewId !== null)\n send(\"webviewTagCloseDevTools\", { id: this.webviewId });\n }\n toggleDevTools() {\n if (this.webviewId !== null)\n send(\"webviewTagToggleDevTools\", { id: this.webviewId });\n }\n executeJavascript(js) {\n if (this.webviewId === null)\n return;\n send(\"webviewTagExecuteJavascript\", { id: this.webviewId, js });\n }\n on(event, listener) {\n if (!this._eventListeners[event])\n this._eventListeners[event] = [];\n this._eventListeners[event].push(listener);\n }\n off(event, listener) {\n if (!this._eventListeners[event])\n return;\n const idx = this._eventListeners[event].indexOf(listener);\n if (idx !== -1)\n this._eventListeners[event].splice(idx, 1);\n }\n emit(event, detail) {\n const listeners = this._eventListeners[event];\n if (listeners) {\n const customEvent = new CustomEvent(event, { detail });\n listeners.forEach((fn) => fn(customEvent));\n }\n }\n get src() {\n return this.getAttribute(\"src\");\n }\n set src(value) {\n if (value) {\n this.setAttribute(\"src\", value);\n } else {\n this.removeAttribute(\"src\");\n }\n }\n get html() {\n return this.getAttribute(\"html\");\n }\n set html(value) {\n if (value) {\n this.setAttribute(\"html\", value);\n } else {\n this.removeAttribute(\"html\");\n }\n }\n get preload() {\n return this.getAttribute(\"preload\");\n }\n set preload(value) {\n if (value)\n this.setAttribute(\"preload\", value);\n else\n this.removeAttribute(\"preload\");\n }\n get renderer() {\n return this.getAttribute(\"renderer\") || \"native\";\n }\n set renderer(value) {\n this.setAttribute(\"renderer\", value);\n }\n get sandbox() {\n return this.sandboxed;\n }\n}\nfunction initWebviewTag() {\n if (!customElements.get(\"electrobun-webview\")) {\n customElements.define(\"electrobun-webview\", ElectrobunWebviewTag);\n }\n const injectStyles = () => {\n const style = document.createElement(\"style\");\n style.textContent = `\nelectrobun-webview {\n\tdisplay: block;\n\twidth: 800px;\n\theight: 300px;\n\tbackground: #fff;\n\tbackground-repeat: no-repeat !important;\n\toverflow: hidden;\n}\n`;\n if (document.head?.firstChild) {\n document.head.insertBefore(style, document.head.firstChild);\n } else if (document.head) {\n document.head.appendChild(style);\n }\n };\n if (document.head) {\n injectStyles();\n } else {\n document.addEventListener(\"DOMContentLoaded\", injectStyles);\n }\n}\n\n// src/preload/wgpuTag.ts\nvar wgpuTagRegistry = {};\n\nclass ElectrobunWgpuTag extends HTMLElement {\n wgpuViewId = null;\n maskSelectors = new Set;\n _sync = null;\n transparent = false;\n passthroughEnabled = false;\n hidden = false;\n _ready = false;\n _initializing = false;\n _eventListeners = {};\n constructor() {\n super();\n }\n connectedCallback() {\n requestAnimationFrame(() => {\n if (this.isConnected)\n this.initWgpuView();\n });\n }\n disconnectedCallback() {\n if (this.wgpuViewId !== null) {\n send(\"wgpuTagRemove\", { id: this.wgpuViewId });\n delete wgpuTagRegistry[this.wgpuViewId];\n this.wgpuViewId = null;\n }\n if (this._sync)\n this._sync.stop();\n this._sync = null;\n this._ready = false;\n }\n async initWgpuView() {\n if (this._initializing || this.wgpuViewId !== null)\n return;\n this._initializing = true;\n const rect = this.getBoundingClientRect();\n const initialRect = {\n x: rect.x,\n y: rect.y,\n width: rect.width,\n height: rect.height\n };\n const transparent = this.hasAttribute(\"transparent\");\n const passthrough = this.hasAttribute(\"passthrough\");\n const hidden = this.hasAttribute(\"hidden\");\n const masks = this.getAttribute(\"masks\");\n this.transparent = transparent;\n this.passthroughEnabled = passthrough;\n this.hidden = hidden;\n if (masks) {\n masks.split(\",\").forEach((s) => this.maskSelectors.add(s.trim()));\n }\n if (transparent)\n this.style.opacity = \"0\";\n if (passthrough)\n this.style.pointerEvents = \"none\";\n try {\n const wgpuViewId = await request(\"wgpuTagInit\", {\n windowId: window.__electrobunWindowId,\n frame: initialRect,\n transparent,\n passthrough\n });\n if (!this.isConnected) {\n send(\"wgpuTagRemove\", { id: wgpuViewId });\n return;\n }\n this.wgpuViewId = wgpuViewId;\n if (!this.id) {\n this.id = `electrobun-wgpu-${wgpuViewId}`;\n }\n wgpuTagRegistry[wgpuViewId] = this;\n this.setupObservers(initialRect);\n this.syncDimensions(true);\n if (hidden) {\n this.toggleHidden(true);\n }\n requestAnimationFrame(() => {\n Object.values(wgpuTagRegistry).forEach((view) => {\n if (view !== this && view.wgpuViewId !== null) {\n view.syncDimensions(true);\n }\n });\n });\n this._ready = true;\n this.emit(\"ready\", { id: wgpuViewId });\n } catch (err) {\n console.error(\"Failed to init WGPU view:\", err);\n } finally {\n this._initializing = false;\n }\n }\n setupObservers(initialRect) {\n const getMasks = () => {\n const rect = this.getBoundingClientRect();\n const masks = [];\n this.maskSelectors.forEach((selector) => {\n try {\n document.querySelectorAll(selector).forEach((el) => {\n const mr = el.getBoundingClientRect();\n masks.push({\n x: mr.x - rect.x,\n y: mr.y - rect.y,\n width: mr.width,\n height: mr.height\n });\n });\n } catch (_e) {}\n });\n return masks;\n };\n this._sync = new OverlaySyncController(this, {\n onSync: (rect, masksJson) => {\n if (this.wgpuViewId === null)\n return;\n send(\"wgpuTagResize\", {\n id: this.wgpuViewId,\n frame: rect,\n masks: masksJson\n });\n },\n getMasks,\n burstIntervalMs: 10,\n baseIntervalMs: 100,\n burstDurationMs: 50\n });\n this._sync.setLastRect(initialRect);\n this._sync.start();\n }\n syncDimensions(force = false) {\n if (!this._sync)\n return;\n if (force) {\n this._sync.forceSync();\n }\n }\n toggleTransparent(value) {\n if (this.wgpuViewId === null)\n return;\n this.transparent = value !== undefined ? value : !this.transparent;\n this.style.opacity = this.transparent ? \"0\" : \"\";\n send(\"wgpuTagSetTransparent\", {\n id: this.wgpuViewId,\n transparent: this.transparent\n });\n }\n togglePassthrough(value) {\n if (this.wgpuViewId === null)\n return;\n this.passthroughEnabled = value !== undefined ? value : !this.passthroughEnabled;\n this.style.pointerEvents = this.passthroughEnabled ? \"none\" : \"\";\n send(\"wgpuTagSetPassthrough\", {\n id: this.wgpuViewId,\n passthrough: this.passthroughEnabled\n });\n }\n toggleHidden(value) {\n if (this.wgpuViewId === null)\n return;\n this.hidden = value !== undefined ? value : !this.hidden;\n send(\"wgpuTagSetHidden\", { id: this.wgpuViewId, hidden: this.hidden });\n }\n runTest() {\n if (this.wgpuViewId === null)\n return;\n send(\"wgpuTagRunTest\", { id: this.wgpuViewId });\n }\n addMaskSelector(selector) {\n this.maskSelectors.add(selector);\n this.syncDimensions(true);\n }\n removeMaskSelector(selector) {\n this.maskSelectors.delete(selector);\n this.syncDimensions(true);\n }\n on(event, listener) {\n if (!this._eventListeners[event])\n this._eventListeners[event] = [];\n this._eventListeners[event].push(listener);\n if (event === \"ready\" && this._ready && this.wgpuViewId !== null) {\n const readyEvent = new CustomEvent(event, {\n detail: { id: this.wgpuViewId }\n });\n queueMicrotask(() => {\n if (this._eventListeners[event]?.includes(listener)) {\n listener(readyEvent);\n }\n });\n }\n }\n off(event, listener) {\n if (!this._eventListeners[event])\n return;\n const idx = this._eventListeners[event].indexOf(listener);\n if (idx !== -1)\n this._eventListeners[event].splice(idx, 1);\n }\n emit(event, detail) {\n const listeners = this._eventListeners[event];\n if (listeners) {\n const customEvent = new CustomEvent(event, { detail });\n listeners.forEach((fn) => fn(customEvent));\n }\n }\n}\nfunction initWgpuTag() {\n if (!customElements.get(\"electrobun-wgpu\")) {\n customElements.define(\"electrobun-wgpu\", ElectrobunWgpuTag);\n }\n const injectStyles = () => {\n const style = document.createElement(\"style\");\n style.textContent = `\nelectrobun-wgpu {\n\tdisplay: block;\n\twidth: 800px;\n\theight: 300px;\n\tbackground: #000;\n\toverflow: hidden;\n}\n`;\n if (document.head?.firstChild) {\n document.head.insertBefore(style, document.head.firstChild);\n } else if (document.head) {\n document.head.appendChild(style);\n }\n };\n if (document.head) {\n injectStyles();\n } else {\n document.addEventListener(\"DOMContentLoaded\", injectStyles);\n }\n}\n\n// src/preload/events.ts\nfunction emitWebviewEvent(eventName, detail) {\n setTimeout(() => {\n const bridge = window.__electrobunEventBridge || window.__electrobunInternalBridge;\n bridge?.postMessage(JSON.stringify({\n id: \"webviewEvent\",\n type: \"message\",\n payload: {\n id: window.__electrobunWebviewId,\n eventName,\n detail\n }\n }));\n });\n}\nfunction initLifecycleEvents() {\n window.addEventListener(\"load\", () => {\n if (window === window.top) {\n emitWebviewEvent(\"dom-ready\", document.location.href);\n }\n });\n window.addEventListener(\"popstate\", () => {\n emitWebviewEvent(\"did-navigate-in-page\", window.location.href);\n });\n window.addEventListener(\"hashchange\", () => {\n emitWebviewEvent(\"did-navigate-in-page\", window.location.href);\n });\n}\nvar cmdKeyHeld = false;\nvar cmdKeyTimestamp = 0;\nvar CMD_KEY_THRESHOLD_MS = 500;\nfunction isCmdHeld() {\n if (cmdKeyHeld)\n return true;\n return Date.now() - cmdKeyTimestamp < CMD_KEY_THRESHOLD_MS && cmdKeyTimestamp > 0;\n}\nfunction initCmdClickHandling() {\n window.addEventListener(\"keydown\", (event) => {\n if (event.key === \"Meta\" || event.metaKey) {\n cmdKeyHeld = true;\n cmdKeyTimestamp = Date.now();\n }\n }, true);\n window.addEventListener(\"keyup\", (event) => {\n if (event.key === \"Meta\") {\n cmdKeyHeld = false;\n cmdKeyTimestamp = Date.now();\n }\n }, true);\n window.addEventListener(\"blur\", () => {\n cmdKeyHeld = false;\n });\n window.addEventListener(\"click\", (event) => {\n if (event.metaKey || event.ctrlKey) {\n const anchor = event.target?.closest?.(\"a\");\n if (anchor && anchor.href) {\n event.preventDefault();\n event.stopPropagation();\n event.stopImmediatePropagation();\n emitWebviewEvent(\"new-window-open\", JSON.stringify({\n url: anchor.href,\n isCmdClick: true,\n isSPANavigation: false\n }));\n }\n }\n }, true);\n}\nfunction initSPANavigationInterception() {\n const originalPushState = history.pushState;\n const originalReplaceState = history.replaceState;\n history.pushState = function(state, title, url) {\n if (isCmdHeld() && url) {\n const resolvedUrl = new URL(String(url), window.location.href).href;\n emitWebviewEvent(\"new-window-open\", JSON.stringify({\n url: resolvedUrl,\n isCmdClick: true,\n isSPANavigation: true\n }));\n return;\n }\n return originalPushState.apply(this, [state, title, url]);\n };\n history.replaceState = function(state, title, url) {\n if (isCmdHeld() && url) {\n const resolvedUrl = new URL(String(url), window.location.href).href;\n emitWebviewEvent(\"new-window-open\", JSON.stringify({\n url: resolvedUrl,\n isCmdClick: true,\n isSPANavigation: true\n }));\n return;\n }\n return originalReplaceState.apply(this, [state, title, url]);\n };\n}\nfunction shouldApplyOverscrollPrevention(platform) {\n return platform !== \"linux\";\n}\nfunction initOverscrollPrevention(targetDocument = document, platform = window.__electrobunPlatform) {\n if (!shouldApplyOverscrollPrevention(platform))\n return;\n targetDocument.addEventListener(\"DOMContentLoaded\", () => {\n const style = targetDocument.createElement(\"style\");\n style.type = \"text/css\";\n style.appendChild(targetDocument.createTextNode(\"html, body { overscroll-behavior: none; }\"));\n targetDocument.head.appendChild(style);\n });\n}\n\n// src/preload/index.ts\ninitEncryption().catch((err) => console.error(\"Failed to initialize encryption:\", err));\nvar internalMessageHandler = (msg) => {\n handleResponse(msg);\n};\nvar defaultUserMessageHandler = (msg) => {\n if (!window.__electrobunPendingHostMessages) {\n window.__electrobunPendingHostMessages = [];\n }\n window.__electrobunPendingHostMessages.push(msg);\n};\nif (!window.__electrobun) {\n window.__electrobun = {\n receiveInternalMessageFromHost: internalMessageHandler,\n receiveMessageFromHost: defaultUserMessageHandler,\n receiveInternalMessageFromBun: internalMessageHandler,\n receiveMessageFromBun: defaultUserMessageHandler\n };\n} else {\n window.__electrobun.receiveInternalMessageFromHost = internalMessageHandler;\n window.__electrobun.receiveMessageFromHost = defaultUserMessageHandler;\n window.__electrobun.receiveInternalMessageFromBun = internalMessageHandler;\n window.__electrobun.receiveMessageFromBun = defaultUserMessageHandler;\n}\nwindow.__electrobunSendToHost = (message) => {\n emitWebviewEvent(\"host-message\", JSON.stringify(message));\n};\ninitLifecycleEvents();\ninitCmdClickHandling();\ninitSPANavigationInterception();\ninitOverscrollPrevention();\ninitDragRegions();\ninitExternalDropFocusRestoration();\ninitWebviewTag();\ninitWgpuTag();\n})();";
5
+ export const preloadScript = "(function(){// src/preload/encryption.ts\nfunction base64ToUint8Array(base64) {\n return new Uint8Array(atob(base64).split(\"\").map((char) => char.charCodeAt(0)));\n}\nfunction uint8ArrayToBase64(uint8Array) {\n let binary = \"\";\n for (let i = 0;i < uint8Array.length; i++) {\n binary += String.fromCharCode(uint8Array[i]);\n }\n return btoa(binary);\n}\nfunction toArrayBuffer(bytes) {\n const buffer = new ArrayBuffer(bytes.byteLength);\n new Uint8Array(buffer).set(bytes);\n return buffer;\n}\nasync function generateKeyFromBytes(rawKey) {\n return await window.crypto.subtle.importKey(\"raw\", toArrayBuffer(rawKey), { name: \"AES-GCM\" }, true, [\"encrypt\", \"decrypt\"]);\n}\nasync function initEncryption() {\n const secretKey = await generateKeyFromBytes(new Uint8Array(window.__electrobunSecretKeyBytes));\n const encryptString = async (plaintext) => {\n const encoder = new TextEncoder;\n const encodedText = encoder.encode(plaintext);\n const iv = window.crypto.getRandomValues(new Uint8Array(12));\n const encryptedBuffer = await window.crypto.subtle.encrypt({ name: \"AES-GCM\", iv: toArrayBuffer(iv) }, secretKey, toArrayBuffer(encodedText));\n const encryptedData = new Uint8Array(encryptedBuffer.slice(0, -16));\n const tag = new Uint8Array(encryptedBuffer.slice(-16));\n return {\n encryptedData: uint8ArrayToBase64(encryptedData),\n iv: uint8ArrayToBase64(iv),\n tag: uint8ArrayToBase64(tag)\n };\n };\n const decryptString = async (encryptedDataB64, ivB64, tagB64) => {\n const encryptedData = base64ToUint8Array(encryptedDataB64);\n const iv = base64ToUint8Array(ivB64);\n const tag = base64ToUint8Array(tagB64);\n const combinedData = new Uint8Array(encryptedData.length + tag.length);\n combinedData.set(encryptedData);\n combinedData.set(tag, encryptedData.length);\n const decryptedBuffer = await window.crypto.subtle.decrypt({ name: \"AES-GCM\", iv: toArrayBuffer(iv) }, secretKey, toArrayBuffer(combinedData));\n const decoder = new TextDecoder;\n return decoder.decode(decryptedBuffer);\n };\n window.__electrobun_encrypt = encryptString;\n window.__electrobun_decrypt = decryptString;\n}\n\n// src/preload/internalRpc.ts\nvar pendingRequests = {};\nvar requestId = 0;\nvar isProcessingQueue = false;\nvar sendQueue = [];\nfunction processQueue() {\n if (isProcessingQueue) {\n setTimeout(processQueue);\n return;\n }\n if (sendQueue.length === 0)\n return;\n isProcessingQueue = true;\n const batch = JSON.stringify(sendQueue);\n sendQueue.length = 0;\n window.__electrobunInternalBridge?.postMessage(batch);\n setTimeout(() => {\n isProcessingQueue = false;\n }, 2);\n}\nfunction send(type, payload) {\n sendQueue.push(JSON.stringify({ type: \"message\", id: type, payload }));\n processQueue();\n}\nfunction request(type, payload) {\n return new Promise((resolve, reject) => {\n const id = `req_${++requestId}_${Date.now()}`;\n pendingRequests[id] = { resolve, reject };\n sendQueue.push(JSON.stringify({\n type: \"request\",\n method: type,\n id,\n params: payload,\n hostWebviewId: window.__electrobunWebviewId\n }));\n processQueue();\n setTimeout(() => {\n if (pendingRequests[id]) {\n delete pendingRequests[id];\n reject(new Error(`Request timeout: ${type}`));\n }\n }, 1e4);\n });\n}\nfunction handleResponse(msg) {\n if (msg && msg.type === \"response\" && msg.id) {\n const pending = pendingRequests[msg.id];\n if (pending) {\n delete pendingRequests[msg.id];\n if (msg.success)\n pending.resolve(msg.payload);\n else\n pending.reject(msg.payload);\n }\n }\n}\n\n// src/preload/dragRegions.ts\nvar DRAG_CLASS = \"electrobun-webkit-app-region-drag\";\nvar NO_DRAG_CLASS = \"electrobun-webkit-app-region-no-drag\";\nvar MIRRORED_PROPERTY = \"--electrobun-app-region\";\nvar MIRROR_ATTRIBUTE = \"data-electrobun-app-region-mirror\";\nvar APP_REGION_PROPERTIES = [\n MIRRORED_PROPERTY,\n \"-webkit-app-region\",\n \"app-region\",\n \"window-drag\"\n];\nvar DRAG_REGION_COMPATIBILITY_CSS = `\n.electrobun-webkit-app-region-drag {\n\t-webkit-app-region: drag;\n\tapp-region: drag;\n}\n.electrobun-webkit-app-region-no-drag {\n\t-webkit-app-region: no-drag;\n\tapp-region: no-drag;\n}\n`;\nvar processedSourceSignatures = new WeakMap;\nvar stylesheetMirrors = new Map;\nvar pendingLinkSignatures = new WeakMap;\nvar stylesheetMirroringInitialized = false;\nfunction normalizedRegion(value) {\n const normalized = value?.trim().toLowerCase();\n if (normalized === \"drag\" || normalized === \"no-drag\")\n return normalized;\n return null;\n}\nfunction previousSignificantCharacter(source, offset) {\n let index = offset - 1;\n while (index >= 0) {\n while (index >= 0 && /\\s/.test(source[index] ?? \"\"))\n index--;\n if (index >= 1 && source[index] === \"/\" && source[index - 1] === \"*\") {\n const commentStart = source.lastIndexOf(\"/*\", index - 1);\n if (commentStart < 0)\n return \"\";\n index = commentStart - 1;\n continue;\n }\n return source[index] ?? \"\";\n }\n return \"\";\n}\nfunction isIdentifierCharacter(character) {\n return !!character && /[a-zA-Z0-9_-]/.test(character);\n}\nfunction rewriteAppRegionDeclarations(source) {\n const propertyNames = [\"-webkit-app-region\", \"window-drag\", \"app-region\"];\n let output = \"\";\n let cursor = 0;\n let index = 0;\n while (index < source.length) {\n if (source[index] === \"/\" && source[index + 1] === \"*\") {\n const commentEnd = source.indexOf(\"*/\", index + 2);\n index = commentEnd < 0 ? source.length : commentEnd + 2;\n continue;\n }\n if (source[index] === '\"' || source[index] === \"'\") {\n const quote = source[index];\n index++;\n while (index < source.length) {\n if (source[index] === \"\\\\\") {\n index += 2;\n continue;\n }\n if (source[index] === quote) {\n index++;\n break;\n }\n index++;\n }\n continue;\n }\n let matchedProperty;\n for (const propertyName of propertyNames) {\n if (source.slice(index, index + propertyName.length).toLowerCase() === propertyName && !isIdentifierCharacter(source[index - 1]) && !isIdentifierCharacter(source[index + propertyName.length])) {\n matchedProperty = propertyName;\n break;\n }\n }\n if (matchedProperty) {\n let colonOffset = index + matchedProperty.length;\n while (/\\s/.test(source[colonOffset] ?? \"\"))\n colonOffset++;\n const previous = previousSignificantCharacter(source, index);\n if (source[colonOffset] === \":\" && (previous === \"{\" || previous === \";\")) {\n output += source.slice(cursor, index) + MIRRORED_PROPERTY;\n index += matchedProperty.length;\n cursor = index;\n continue;\n }\n }\n index++;\n }\n return output + source.slice(cursor);\n}\nfunction nestedRuleContainer(rule) {\n const candidate = rule;\n if (candidate.cssRules && typeof candidate.deleteRule === \"function\") {\n return candidate;\n }\n return null;\n}\nfunction pruneToAppRegionRules(container) {\n let hasAppRegionRule = false;\n for (let index = container.cssRules.length - 1;index >= 0; index--) {\n const rule = container.cssRules[index];\n if (!rule)\n continue;\n const style = rule.style;\n let hasAppRegionDeclaration = false;\n if (style && typeof style.getPropertyValue === \"function\") {\n for (let propertyIndex = style.length - 1;propertyIndex >= 0; propertyIndex--) {\n const propertyName = style.item(propertyIndex);\n if (propertyName === MIRRORED_PROPERTY) {\n hasAppRegionDeclaration = true;\n } else {\n style.removeProperty(propertyName);\n }\n }\n }\n const nested = nestedRuleContainer(rule);\n const hasNestedAppRegionRule = nested ? pruneToAppRegionRules(nested) : false;\n if (!hasAppRegionDeclaration && !hasNestedAppRegionRule) {\n container.deleteRule(index);\n continue;\n }\n hasAppRegionRule = true;\n }\n return hasAppRegionRule;\n}\nfunction serializeRules(rules) {\n let cssText = \"\";\n for (let index = 0;index < rules.length; index++) {\n const rule = rules[index];\n if (rule)\n cssText += `${rule.cssText}\n`;\n }\n return cssText;\n}\nfunction removeStylesheetMirror(source) {\n stylesheetMirrors.get(source)?.remove();\n stylesheetMirrors.delete(source);\n}\nfunction installStylesheetMirror(source, cssText, signature) {\n if (processedSourceSignatures.get(source) === signature)\n return;\n processedSourceSignatures.set(source, signature);\n removeStylesheetMirror(source);\n const mirroredCss = rewriteAppRegionDeclarations(cssText);\n if (mirroredCss === cssText || !source.parentNode)\n return;\n const mirror = source.ownerDocument.createElement(\"style\");\n mirror.setAttribute(MIRROR_ATTRIBUTE, \"\");\n mirror.media = \"not all\";\n if (source.nonce)\n mirror.nonce = source.nonce;\n mirror.textContent = mirroredCss;\n source.parentNode.insertBefore(mirror, source.nextSibling);\n let sheet = mirror.sheet;\n if (!sheet || !pruneToAppRegionRules(sheet)) {\n mirror.remove();\n return;\n }\n mirror.textContent = serializeRules(sheet.cssRules);\n sheet = mirror.sheet;\n if (!sheet) {\n mirror.remove();\n return;\n }\n mirror.media = source.media;\n if (source.sheet?.disabled)\n sheet.disabled = true;\n stylesheetMirrors.set(source, mirror);\n}\nfunction isMirroredStyle(element) {\n return element.tagName === \"STYLE\" && element.hasAttribute(MIRROR_ATTRIBUTE);\n}\nfunction isStylesheetSource(element) {\n if (element.tagName === \"STYLE\")\n return !isMirroredStyle(element);\n return element.tagName === \"LINK\" && element.relList.contains(\"stylesheet\");\n}\nfunction nodeContainsStylesheetSource(node) {\n const element = node;\n if (typeof element.matches !== \"function\")\n return false;\n return isStylesheetSource(element) || !!element.querySelector(\"style:not([data-electrobun-app-region-mirror]), link[rel~='stylesheet']\");\n}\nfunction mutationsAffectStylesheets(mutations) {\n for (const mutation of mutations) {\n if (mutation.type === \"attributes\") {\n const element = mutation.target;\n if (element.tagName === \"LINK\" || isStylesheetSource(element))\n return true;\n continue;\n }\n if (mutation.type === \"characterData\") {\n const parent = mutation.target.parentElement;\n if (parent && isStylesheetSource(parent))\n return true;\n continue;\n }\n const target = mutation.target;\n if (typeof target.matches === \"function\" && isStylesheetSource(target)) {\n return true;\n }\n for (let index = 0;index < mutation.addedNodes.length; index++) {\n const node = mutation.addedNodes[index];\n if (node && nodeContainsStylesheetSource(node))\n return true;\n }\n for (let index = 0;index < mutation.removedNodes.length; index++) {\n const node = mutation.removedNodes[index];\n if (node && nodeContainsStylesheetSource(node))\n return true;\n }\n }\n return false;\n}\nfunction processStyleElement(style) {\n if (isMirroredStyle(style))\n return;\n const cssText = style.textContent ?? \"\";\n installStylesheetMirror(style, cssText, `${style.media}\n${cssText}`);\n}\nfunction processLinkElement(link) {\n if (!link.relList.contains(\"stylesheet\") || !link.href) {\n removeStylesheetMirror(link);\n return;\n }\n const signature = `${link.media}\n${link.href}`;\n if (processedSourceSignatures.get(link) === signature || pendingLinkSignatures.get(link) === signature) {\n return;\n }\n pendingLinkSignatures.set(link, signature);\n fetch(link.href, { credentials: \"same-origin\" }).then((response) => {\n if (!response.ok)\n throw new Error(`HTTP ${response.status}`);\n return response.text();\n }).then((cssText) => {\n if (link.isConnected && `${link.media}\n${link.href}` === signature) {\n installStylesheetMirror(link, cssText, signature);\n }\n }).catch(() => {}).finally(() => {\n if (pendingLinkSignatures.get(link) === signature) {\n pendingLinkSignatures.delete(link);\n }\n });\n}\nfunction scanStylesheetSources() {\n document.querySelectorAll(\"style, link[rel~='stylesheet']\").forEach((element) => {\n if (element.tagName === \"STYLE\") {\n processStyleElement(element);\n } else {\n processLinkElement(element);\n }\n });\n for (const [source, mirror] of stylesheetMirrors) {\n if (!source.isConnected) {\n mirror.remove();\n stylesheetMirrors.delete(source);\n }\n }\n}\nfunction initStylesheetMirroring() {\n if (stylesheetMirroringInitialized)\n return;\n stylesheetMirroringInitialized = true;\n let scanScheduled = false;\n const scheduleScan = () => {\n if (scanScheduled)\n return;\n scanScheduled = true;\n queueMicrotask(() => {\n scanScheduled = false;\n scanStylesheetSources();\n });\n };\n new MutationObserver((mutations) => {\n if (mutationsAffectStylesheets(mutations))\n scheduleScan();\n }).observe(document, {\n attributes: true,\n attributeFilter: [\"href\", \"media\", \"rel\"],\n characterData: true,\n childList: true,\n subtree: true\n });\n document.addEventListener(\"DOMContentLoaded\", scheduleScan, { once: true });\n scheduleScan();\n}\nfunction installCompatibilityStyles() {\n if (document.querySelector(\"style[data-electrobun-drag-region-compat]\"))\n return;\n const style = document.createElement(\"style\");\n style.setAttribute(\"data-electrobun-drag-region-compat\", \"\");\n style.textContent = DRAG_REGION_COMPATIBILITY_CSS;\n const install = () => {\n if (!document.head || style.isConnected)\n return;\n document.head.insertBefore(style, document.head.firstChild);\n };\n if (document.head)\n install();\n else\n document.addEventListener(\"DOMContentLoaded\", install, { once: true });\n}\nfunction inlineRegion(element) {\n const style = element.getAttribute?.(\"style\");\n if (!style)\n return null;\n const match = style.match(/(?:^|;)\\s*(?:-webkit-app-region|app-region|window-drag)\\s*:\\s*(no-drag|drag)\\b/i);\n return normalizedRegion(match?.[1]);\n}\nfunction computedRegion(element, readComputedStyle) {\n try {\n const style = readComputedStyle(element);\n for (const propertyName of APP_REGION_PROPERTIES) {\n const region = normalizedRegion(style.getPropertyValue(propertyName));\n if (region)\n return region;\n }\n } catch {}\n return null;\n}\nfunction eventTargetElement(target) {\n if (!target || typeof target.getAttribute !== \"function\") {\n return target?.parentElement ?? null;\n }\n return target;\n}\nfunction isAppRegionDragTarget(target, readComputedStyle = (element) => window.getComputedStyle(element)) {\n let element = eventTargetElement(target);\n let foundDragRegion = false;\n while (element) {\n if (element.classList?.contains(NO_DRAG_CLASS))\n return false;\n if (element.classList?.contains(DRAG_CLASS))\n foundDragRegion = true;\n const region = inlineRegion(element) ?? computedRegion(element, readComputedStyle);\n if (region === \"no-drag\")\n return false;\n if (region === \"drag\")\n foundDragRegion = true;\n element = element.parentElement;\n }\n return foundDragRegion;\n}\nfunction registerDragRegionListeners(targetDocument, getWindowId, sendMessage = send, readComputedStyle = (element) => window.getComputedStyle(element)) {\n targetDocument.addEventListener(\"mousedown\", (event) => {\n if (isAppRegionDragTarget(event.target, readComputedStyle)) {\n sendMessage(\"startWindowMove\", { id: getWindowId() });\n }\n });\n targetDocument.addEventListener(\"mouseup\", (event) => {\n if (isAppRegionDragTarget(event.target, readComputedStyle)) {\n sendMessage(\"stopWindowMove\", { id: getWindowId() });\n }\n });\n}\nfunction initDragRegions() {\n installCompatibilityStyles();\n initStylesheetMirroring();\n registerDragRegionListeners(document, () => window.__electrobunWindowId);\n}\n\n// src/preload/externalDropFocus.ts\nfunction initExternalDropFocusRestoration(targetWindow = window, platform = window.__electrobunPlatform, requestFocus = request, schedule = (callback) => setTimeout(callback)) {\n if (platform !== \"windows\")\n return;\n targetWindow.addEventListener(\"drop\", (event) => {\n const types = Array.from(event.dataTransfer?.types ?? []);\n if (!types.includes(\"Files\"))\n return;\n const previouslyFocusedElement = targetWindow.document.activeElement;\n schedule(() => {\n requestFocus(\"restoreWindowFocusAfterExternalDrop\", {\n windowId: targetWindow.__electrobunWindowId\n }).catch(() => {\n return;\n }).then(() => {\n targetWindow.focus();\n if (previouslyFocusedElement && \"focus\" in previouslyFocusedElement && typeof previouslyFocusedElement.focus === \"function\") {\n previouslyFocusedElement.focus();\n }\n });\n });\n }, true);\n}\n\n// src/preload/overlaySync.ts\nclass OverlaySyncController {\n element;\n options;\n lastRect = { x: 0, y: 0, width: 0, height: 0 };\n resizeObserver = null;\n positionLoop = null;\n resizeHandler = null;\n burstUntil = 0;\n constructor(element, options) {\n this.element = element;\n this.options = {\n onSync: options.onSync,\n getMasks: options.getMasks ?? (() => []),\n burstIntervalMs: options.burstIntervalMs ?? 50,\n baseIntervalMs: options.baseIntervalMs ?? 100,\n burstDurationMs: options.burstDurationMs ?? 500\n };\n }\n start() {\n this.resizeObserver = new ResizeObserver(() => this.sync());\n this.resizeObserver.observe(this.element);\n const loop = () => {\n this.sync();\n const now = performance.now();\n const interval = now < this.burstUntil ? this.options.burstIntervalMs : this.options.baseIntervalMs;\n this.positionLoop = setTimeout(loop, interval);\n };\n this.positionLoop = setTimeout(loop, this.options.baseIntervalMs);\n this.resizeHandler = () => this.sync(true);\n window.addEventListener(\"resize\", this.resizeHandler);\n }\n stop() {\n if (this.resizeObserver)\n this.resizeObserver.disconnect();\n if (this.positionLoop)\n clearTimeout(this.positionLoop);\n if (this.resizeHandler) {\n window.removeEventListener(\"resize\", this.resizeHandler);\n }\n this.resizeObserver = null;\n this.positionLoop = null;\n this.resizeHandler = null;\n }\n forceSync() {\n this.sync(true);\n }\n setLastRect(rect) {\n this.lastRect = rect;\n }\n sync(force = false) {\n const rect = this.element.getBoundingClientRect();\n const newRect = {\n x: rect.x,\n y: rect.y,\n width: rect.width,\n height: rect.height\n };\n if (newRect.width === 0 && newRect.height === 0) {\n return;\n }\n if (!force && newRect.x === this.lastRect.x && newRect.y === this.lastRect.y && newRect.width === this.lastRect.width && newRect.height === this.lastRect.height) {\n return;\n }\n this.burstUntil = performance.now() + this.options.burstDurationMs;\n this.lastRect = newRect;\n const masks = this.options.getMasks();\n this.options.onSync(newRect, JSON.stringify(masks));\n }\n}\n\n// src/preload/webviewTagNavigation.ts\nclass WebviewTagNavigationQueue {\n pending = null;\n beginInitialization() {\n this.pending = null;\n }\n defer(navigation) {\n this.pending = navigation;\n }\n take() {\n const navigation = this.pending;\n this.pending = null;\n return navigation;\n }\n}\n\n// src/preload/webviewTag.ts\nvar webviewRegistry = {};\n\nclass ElectrobunWebviewTag extends HTMLElement {\n webviewId = null;\n maskSelectors = new Set;\n _sync = null;\n _navigationQueue = new WebviewTagNavigationQueue;\n _initializationStarted = false;\n transparent = false;\n passthroughEnabled = false;\n spellCheckEnabled = false;\n hidden = false;\n sandboxed = false;\n _eventListeners = {};\n static get observedAttributes() {\n return [\"src\", \"html\", \"spellcheck\"];\n }\n constructor() {\n super();\n }\n connectedCallback() {\n requestAnimationFrame(() => this.initWebview());\n }\n attributeChangedCallback(name, oldValue, newValue) {\n if (oldValue === newValue)\n return;\n if (name === \"src\" && newValue !== null) {\n this.navigate({ kind: \"url\", value: newValue });\n } else if (name === \"html\" && newValue !== null) {\n this.navigate({ kind: \"html\", value: newValue });\n } else if (name === \"spellcheck\" && this.webviewId !== null) {\n this.setSpellCheck(newValue !== null && newValue.toLowerCase() !== \"false\");\n }\n }\n disconnectedCallback() {\n if (this.webviewId !== null) {\n send(\"webviewTagRemove\", { id: this.webviewId });\n delete webviewRegistry[this.webviewId];\n }\n if (this._sync)\n this._sync.stop();\n }\n getInitialNavigationRules() {\n const rawRules = this.getAttribute(\"navigation-rules\");\n if (rawRules === null) {\n return null;\n }\n const trimmed = rawRules.trim();\n if (!trimmed) {\n return [];\n }\n try {\n const parsed = JSON.parse(trimmed);\n if (!Array.isArray(parsed) || !parsed.every((rule) => typeof rule === \"string\")) {\n throw new Error(\"navigation-rules must be a JSON string array\");\n }\n return parsed;\n } catch (error) {\n console.error(\"Invalid navigation-rules attribute:\", error);\n return [];\n }\n }\n async initWebview() {\n this._initializationStarted = true;\n this._navigationQueue.beginInitialization();\n const rect = this.getBoundingClientRect();\n const initialRect = {\n x: rect.x,\n y: rect.y,\n width: rect.width,\n height: rect.height\n };\n const url = this.getAttribute(\"src\");\n const html = this.getAttribute(\"html\");\n const preload = this.getAttribute(\"preload\");\n const partition = this.getAttribute(\"partition\");\n const renderer = this.getAttribute(\"renderer\") || \"native\";\n const masks = this.getAttribute(\"masks\");\n const navigationRules = this.getInitialNavigationRules();\n const sandbox = this.hasAttribute(\"sandbox\");\n this.sandboxed = sandbox;\n const transparent = this.hasAttribute(\"transparent\");\n const passthrough = this.hasAttribute(\"passthrough\");\n const spellCheckAttribute = this.getAttribute(\"spellcheck\");\n const spellCheck = spellCheckAttribute !== null && spellCheckAttribute.toLowerCase() !== \"false\";\n this.transparent = transparent;\n this.passthroughEnabled = passthrough;\n this.spellCheckEnabled = spellCheck;\n if (transparent)\n this.style.opacity = \"0\";\n if (passthrough)\n this.style.pointerEvents = \"none\";\n if (masks) {\n masks.split(\",\").forEach((s) => this.maskSelectors.add(s.trim()));\n }\n try {\n const webviewInitParams = {\n hostWebviewId: window.__electrobunWebviewId,\n windowId: window.__electrobunWindowId,\n renderer,\n url,\n html,\n preload,\n partition,\n frame: initialRect,\n sandbox,\n transparent,\n passthrough,\n spellCheck,\n ...navigationRules === null ? {} : { navigationRules }\n };\n const webviewId = await request(\"webviewTagInit\", webviewInitParams);\n this.webviewId = webviewId;\n this.id = `electrobun-webview-${webviewId}`;\n webviewRegistry[webviewId] = this;\n const pendingNavigation = this._navigationQueue.take();\n if (pendingNavigation)\n this.navigate(pendingNavigation);\n this.setupObservers(initialRect);\n this.syncDimensions(true);\n requestAnimationFrame(() => {\n Object.values(webviewRegistry).forEach((webview) => {\n if (webview !== this && webview.webviewId !== null) {\n webview.syncDimensions(true);\n }\n });\n });\n } catch (err) {\n console.error(\"Failed to init webview:\", err);\n }\n }\n setupObservers(initialRect) {\n const getMasks = () => {\n const rect = this.getBoundingClientRect();\n const masks = [];\n this.maskSelectors.forEach((selector) => {\n try {\n document.querySelectorAll(selector).forEach((el) => {\n const mr = el.getBoundingClientRect();\n masks.push({\n x: mr.x - rect.x,\n y: mr.y - rect.y,\n width: mr.width,\n height: mr.height\n });\n });\n } catch (_e) {}\n });\n return masks;\n };\n this._sync = new OverlaySyncController(this, {\n onSync: (rect, masksJson) => {\n if (this.webviewId === null)\n return;\n send(\"webviewTagResize\", {\n id: this.webviewId,\n frame: rect,\n masks: masksJson\n });\n },\n getMasks,\n burstIntervalMs: 10,\n baseIntervalMs: 100,\n burstDurationMs: 50\n });\n this._sync.setLastRect(initialRect);\n this._sync.start();\n }\n syncDimensions(force = false) {\n if (!this._sync)\n return;\n if (force) {\n this._sync.forceSync();\n }\n }\n navigate(navigation) {\n if (this.webviewId === null) {\n this._navigationQueue.defer(navigation);\n return;\n }\n if (navigation.kind === \"url\") {\n send(\"webviewTagUpdateSrc\", {\n id: this.webviewId,\n url: navigation.value\n });\n } else {\n send(\"webviewTagUpdateHtml\", {\n id: this.webviewId,\n html: navigation.value\n });\n }\n }\n loadURL(url) {\n if (this.getAttribute(\"src\") === url) {\n this.navigate({ kind: \"url\", value: url });\n } else {\n this.setAttribute(\"src\", url);\n }\n }\n loadHTML(html) {\n if (this.webviewId === null && !this._initializationStarted) {\n this.setAttribute(\"html\", html);\n return;\n }\n this.navigate({ kind: \"html\", value: html });\n }\n reload() {\n if (this.webviewId !== null)\n send(\"webviewTagReload\", { id: this.webviewId });\n }\n goBack() {\n if (this.webviewId !== null)\n send(\"webviewTagGoBack\", { id: this.webviewId });\n }\n goForward() {\n if (this.webviewId !== null)\n send(\"webviewTagGoForward\", { id: this.webviewId });\n }\n async canGoBack() {\n if (this.webviewId === null)\n return false;\n return await request(\"webviewTagCanGoBack\", {\n id: this.webviewId\n });\n }\n async canGoForward() {\n if (this.webviewId === null)\n return false;\n return await request(\"webviewTagCanGoForward\", {\n id: this.webviewId\n });\n }\n async setSpellCheck(enabled) {\n this.spellCheckEnabled = enabled;\n if (this.webviewId === null)\n return false;\n return await request(\"webviewTagSetSpellCheck\", {\n id: this.webviewId,\n enabled\n });\n }\n toggleTransparent(value) {\n if (this.webviewId === null)\n return;\n this.transparent = value !== undefined ? value : !this.transparent;\n this.style.opacity = this.transparent ? \"0\" : \"\";\n send(\"webviewTagSetTransparent\", {\n id: this.webviewId,\n transparent: this.transparent\n });\n }\n togglePassthrough(value) {\n if (this.webviewId === null)\n return;\n this.passthroughEnabled = value !== undefined ? value : !this.passthroughEnabled;\n this.style.pointerEvents = this.passthroughEnabled ? \"none\" : \"\";\n send(\"webviewTagSetPassthrough\", {\n id: this.webviewId,\n enablePassthrough: this.passthroughEnabled\n });\n }\n toggleHidden(value) {\n if (this.webviewId === null)\n return;\n this.hidden = value !== undefined ? value : !this.hidden;\n send(\"webviewTagSetHidden\", { id: this.webviewId, hidden: this.hidden });\n }\n addMaskSelector(selector) {\n this.maskSelectors.add(selector);\n this.syncDimensions(true);\n }\n removeMaskSelector(selector) {\n this.maskSelectors.delete(selector);\n this.syncDimensions(true);\n }\n setNavigationRules(rules) {\n if (this.webviewId !== null) {\n send(\"webviewTagSetNavigationRules\", { id: this.webviewId, rules });\n }\n }\n findInPage(searchText, options) {\n if (this.webviewId === null)\n return;\n const forward = options?.forward !== false;\n const matchCase = options?.matchCase || false;\n send(\"webviewTagFindInPage\", {\n id: this.webviewId,\n searchText,\n forward,\n matchCase\n });\n }\n stopFindInPage() {\n if (this.webviewId !== null)\n send(\"webviewTagStopFind\", { id: this.webviewId });\n }\n openDevTools() {\n if (this.webviewId !== null)\n send(\"webviewTagOpenDevTools\", { id: this.webviewId });\n }\n closeDevTools() {\n if (this.webviewId !== null)\n send(\"webviewTagCloseDevTools\", { id: this.webviewId });\n }\n toggleDevTools() {\n if (this.webviewId !== null)\n send(\"webviewTagToggleDevTools\", { id: this.webviewId });\n }\n executeJavascript(js) {\n if (this.webviewId === null)\n return;\n send(\"webviewTagExecuteJavascript\", { id: this.webviewId, js });\n }\n on(event, listener) {\n if (!this._eventListeners[event])\n this._eventListeners[event] = [];\n this._eventListeners[event].push(listener);\n }\n off(event, listener) {\n if (!this._eventListeners[event])\n return;\n const idx = this._eventListeners[event].indexOf(listener);\n if (idx !== -1)\n this._eventListeners[event].splice(idx, 1);\n }\n emit(event, detail) {\n const listeners = this._eventListeners[event];\n if (listeners) {\n const customEvent = new CustomEvent(event, { detail });\n listeners.forEach((fn) => fn(customEvent));\n }\n }\n get src() {\n return this.getAttribute(\"src\");\n }\n set src(value) {\n if (value) {\n this.setAttribute(\"src\", value);\n } else {\n this.removeAttribute(\"src\");\n }\n }\n get html() {\n return this.getAttribute(\"html\");\n }\n set html(value) {\n if (value) {\n this.setAttribute(\"html\", value);\n } else {\n this.removeAttribute(\"html\");\n }\n }\n get preload() {\n return this.getAttribute(\"preload\");\n }\n set preload(value) {\n if (value)\n this.setAttribute(\"preload\", value);\n else\n this.removeAttribute(\"preload\");\n }\n get renderer() {\n return this.getAttribute(\"renderer\") || \"native\";\n }\n set renderer(value) {\n this.setAttribute(\"renderer\", value);\n }\n get sandbox() {\n return this.sandboxed;\n }\n}\nfunction initWebviewTag() {\n if (!customElements.get(\"electrobun-webview\")) {\n customElements.define(\"electrobun-webview\", ElectrobunWebviewTag);\n }\n const injectStyles = () => {\n const style = document.createElement(\"style\");\n style.textContent = `\nelectrobun-webview {\n\tdisplay: block;\n\twidth: 800px;\n\theight: 300px;\n\tbackground: #fff;\n\tbackground-repeat: no-repeat !important;\n\toverflow: hidden;\n}\n`;\n if (document.head?.firstChild) {\n document.head.insertBefore(style, document.head.firstChild);\n } else if (document.head) {\n document.head.appendChild(style);\n }\n };\n if (document.head) {\n injectStyles();\n } else {\n document.addEventListener(\"DOMContentLoaded\", injectStyles);\n }\n}\n\n// src/preload/wgpuTag.ts\nvar wgpuTagRegistry = {};\n\nclass ElectrobunWgpuTag extends HTMLElement {\n wgpuViewId = null;\n maskSelectors = new Set;\n _sync = null;\n transparent = false;\n passthroughEnabled = false;\n hidden = false;\n _ready = false;\n _initializing = false;\n _eventListeners = {};\n constructor() {\n super();\n }\n connectedCallback() {\n requestAnimationFrame(() => {\n if (this.isConnected)\n this.initWgpuView();\n });\n }\n disconnectedCallback() {\n if (this.wgpuViewId !== null) {\n send(\"wgpuTagRemove\", { id: this.wgpuViewId });\n delete wgpuTagRegistry[this.wgpuViewId];\n this.wgpuViewId = null;\n }\n if (this._sync)\n this._sync.stop();\n this._sync = null;\n this._ready = false;\n }\n async initWgpuView() {\n if (this._initializing || this.wgpuViewId !== null)\n return;\n this._initializing = true;\n const rect = this.getBoundingClientRect();\n const initialRect = {\n x: rect.x,\n y: rect.y,\n width: rect.width,\n height: rect.height\n };\n const transparent = this.hasAttribute(\"transparent\");\n const passthrough = this.hasAttribute(\"passthrough\");\n const hidden = this.hasAttribute(\"hidden\");\n const masks = this.getAttribute(\"masks\");\n this.transparent = transparent;\n this.passthroughEnabled = passthrough;\n this.hidden = hidden;\n if (masks) {\n masks.split(\",\").forEach((s) => this.maskSelectors.add(s.trim()));\n }\n if (transparent)\n this.style.opacity = \"0\";\n if (passthrough)\n this.style.pointerEvents = \"none\";\n try {\n const wgpuViewId = await request(\"wgpuTagInit\", {\n windowId: window.__electrobunWindowId,\n frame: initialRect,\n transparent,\n passthrough\n });\n if (!this.isConnected) {\n send(\"wgpuTagRemove\", { id: wgpuViewId });\n return;\n }\n this.wgpuViewId = wgpuViewId;\n if (!this.id) {\n this.id = `electrobun-wgpu-${wgpuViewId}`;\n }\n wgpuTagRegistry[wgpuViewId] = this;\n this.setupObservers(initialRect);\n this.syncDimensions(true);\n if (hidden) {\n this.toggleHidden(true);\n }\n requestAnimationFrame(() => {\n Object.values(wgpuTagRegistry).forEach((view) => {\n if (view !== this && view.wgpuViewId !== null) {\n view.syncDimensions(true);\n }\n });\n });\n this._ready = true;\n this.emit(\"ready\", { id: wgpuViewId });\n } catch (err) {\n console.error(\"Failed to init WGPU view:\", err);\n } finally {\n this._initializing = false;\n }\n }\n setupObservers(initialRect) {\n const getMasks = () => {\n const rect = this.getBoundingClientRect();\n const masks = [];\n this.maskSelectors.forEach((selector) => {\n try {\n document.querySelectorAll(selector).forEach((el) => {\n const mr = el.getBoundingClientRect();\n masks.push({\n x: mr.x - rect.x,\n y: mr.y - rect.y,\n width: mr.width,\n height: mr.height\n });\n });\n } catch (_e) {}\n });\n return masks;\n };\n this._sync = new OverlaySyncController(this, {\n onSync: (rect, masksJson) => {\n if (this.wgpuViewId === null)\n return;\n send(\"wgpuTagResize\", {\n id: this.wgpuViewId,\n frame: rect,\n masks: masksJson\n });\n },\n getMasks,\n burstIntervalMs: 10,\n baseIntervalMs: 100,\n burstDurationMs: 50\n });\n this._sync.setLastRect(initialRect);\n this._sync.start();\n }\n syncDimensions(force = false) {\n if (!this._sync)\n return;\n if (force) {\n this._sync.forceSync();\n }\n }\n toggleTransparent(value) {\n if (this.wgpuViewId === null)\n return;\n this.transparent = value !== undefined ? value : !this.transparent;\n this.style.opacity = this.transparent ? \"0\" : \"\";\n send(\"wgpuTagSetTransparent\", {\n id: this.wgpuViewId,\n transparent: this.transparent\n });\n }\n togglePassthrough(value) {\n if (this.wgpuViewId === null)\n return;\n this.passthroughEnabled = value !== undefined ? value : !this.passthroughEnabled;\n this.style.pointerEvents = this.passthroughEnabled ? \"none\" : \"\";\n send(\"wgpuTagSetPassthrough\", {\n id: this.wgpuViewId,\n passthrough: this.passthroughEnabled\n });\n }\n toggleHidden(value) {\n if (this.wgpuViewId === null)\n return;\n this.hidden = value !== undefined ? value : !this.hidden;\n send(\"wgpuTagSetHidden\", { id: this.wgpuViewId, hidden: this.hidden });\n }\n runTest() {\n if (this.wgpuViewId === null)\n return;\n send(\"wgpuTagRunTest\", { id: this.wgpuViewId });\n }\n addMaskSelector(selector) {\n this.maskSelectors.add(selector);\n this.syncDimensions(true);\n }\n removeMaskSelector(selector) {\n this.maskSelectors.delete(selector);\n this.syncDimensions(true);\n }\n on(event, listener) {\n if (!this._eventListeners[event])\n this._eventListeners[event] = [];\n this._eventListeners[event].push(listener);\n if (event === \"ready\" && this._ready && this.wgpuViewId !== null) {\n const readyEvent = new CustomEvent(event, {\n detail: { id: this.wgpuViewId }\n });\n queueMicrotask(() => {\n if (this._eventListeners[event]?.includes(listener)) {\n listener(readyEvent);\n }\n });\n }\n }\n off(event, listener) {\n if (!this._eventListeners[event])\n return;\n const idx = this._eventListeners[event].indexOf(listener);\n if (idx !== -1)\n this._eventListeners[event].splice(idx, 1);\n }\n emit(event, detail) {\n const listeners = this._eventListeners[event];\n if (listeners) {\n const customEvent = new CustomEvent(event, { detail });\n listeners.forEach((fn) => fn(customEvent));\n }\n }\n}\nfunction initWgpuTag() {\n if (!customElements.get(\"electrobun-wgpu\")) {\n customElements.define(\"electrobun-wgpu\", ElectrobunWgpuTag);\n }\n const injectStyles = () => {\n const style = document.createElement(\"style\");\n style.textContent = `\nelectrobun-wgpu {\n\tdisplay: block;\n\twidth: 800px;\n\theight: 300px;\n\tbackground: #000;\n\toverflow: hidden;\n}\n`;\n if (document.head?.firstChild) {\n document.head.insertBefore(style, document.head.firstChild);\n } else if (document.head) {\n document.head.appendChild(style);\n }\n };\n if (document.head) {\n injectStyles();\n } else {\n document.addEventListener(\"DOMContentLoaded\", injectStyles);\n }\n}\n\n// src/preload/events.ts\nfunction emitWebviewEvent(eventName, detail) {\n setTimeout(() => {\n const bridge = window.__electrobunEventBridge || window.__electrobunInternalBridge;\n bridge?.postMessage(JSON.stringify({\n id: \"webviewEvent\",\n type: \"message\",\n payload: {\n id: window.__electrobunWebviewId,\n eventName,\n detail\n }\n }));\n });\n}\nfunction initHostMessageBridge(targetWindow = window, emit = emitWebviewEvent) {\n targetWindow.__electrobunSendToHost = (message) => {\n emit(\"host-message\", JSON.stringify(message));\n };\n}\nfunction initLifecycleEvents() {\n window.addEventListener(\"load\", () => {\n if (window === window.top) {\n emitWebviewEvent(\"dom-ready\", document.location.href);\n }\n });\n window.addEventListener(\"popstate\", () => {\n emitWebviewEvent(\"did-navigate-in-page\", window.location.href);\n });\n window.addEventListener(\"hashchange\", () => {\n emitWebviewEvent(\"did-navigate-in-page\", window.location.href);\n });\n}\nvar cmdKeyHeld = false;\nvar cmdKeyTimestamp = 0;\nvar CMD_KEY_THRESHOLD_MS = 500;\nfunction isCmdHeld() {\n if (cmdKeyHeld)\n return true;\n return Date.now() - cmdKeyTimestamp < CMD_KEY_THRESHOLD_MS && cmdKeyTimestamp > 0;\n}\nfunction initCmdClickHandling() {\n window.addEventListener(\"keydown\", (event) => {\n if (event.key === \"Meta\" || event.metaKey) {\n cmdKeyHeld = true;\n cmdKeyTimestamp = Date.now();\n }\n }, true);\n window.addEventListener(\"keyup\", (event) => {\n if (event.key === \"Meta\") {\n cmdKeyHeld = false;\n cmdKeyTimestamp = Date.now();\n }\n }, true);\n window.addEventListener(\"blur\", () => {\n cmdKeyHeld = false;\n });\n window.addEventListener(\"click\", (event) => {\n if (event.metaKey || event.ctrlKey) {\n const anchor = event.target?.closest?.(\"a\");\n if (anchor && anchor.href) {\n event.preventDefault();\n event.stopPropagation();\n event.stopImmediatePropagation();\n emitWebviewEvent(\"new-window-open\", JSON.stringify({\n url: anchor.href,\n isCmdClick: true,\n isSPANavigation: false\n }));\n }\n }\n }, true);\n}\nfunction initSPANavigationInterception() {\n const originalPushState = history.pushState;\n const originalReplaceState = history.replaceState;\n history.pushState = function(state, title, url) {\n if (isCmdHeld() && url) {\n const resolvedUrl = new URL(String(url), window.location.href).href;\n emitWebviewEvent(\"new-window-open\", JSON.stringify({\n url: resolvedUrl,\n isCmdClick: true,\n isSPANavigation: true\n }));\n return;\n }\n return originalPushState.apply(this, [state, title, url]);\n };\n history.replaceState = function(state, title, url) {\n if (isCmdHeld() && url) {\n const resolvedUrl = new URL(String(url), window.location.href).href;\n emitWebviewEvent(\"new-window-open\", JSON.stringify({\n url: resolvedUrl,\n isCmdClick: true,\n isSPANavigation: true\n }));\n return;\n }\n return originalReplaceState.apply(this, [state, title, url]);\n };\n}\nfunction shouldApplyOverscrollPrevention(platform) {\n return platform !== \"linux\";\n}\nfunction initOverscrollPrevention(targetDocument = document, platform = window.__electrobunPlatform) {\n if (!shouldApplyOverscrollPrevention(platform))\n return;\n targetDocument.addEventListener(\"DOMContentLoaded\", () => {\n const style = targetDocument.createElement(\"style\");\n style.type = \"text/css\";\n style.appendChild(targetDocument.createTextNode(\"html, body { overscroll-behavior: none; }\"));\n targetDocument.head.appendChild(style);\n });\n}\n\n// src/preload/index.ts\ninitEncryption().catch((err) => console.error(\"Failed to initialize encryption:\", err));\nvar internalMessageHandler = (msg) => {\n handleResponse(msg);\n};\nvar defaultUserMessageHandler = (msg) => {\n if (!window.__electrobunPendingHostMessages) {\n window.__electrobunPendingHostMessages = [];\n }\n window.__electrobunPendingHostMessages.push(msg);\n};\nif (!window.__electrobun) {\n window.__electrobun = {\n receiveInternalMessageFromHost: internalMessageHandler,\n receiveMessageFromHost: defaultUserMessageHandler,\n receiveInternalMessageFromBun: internalMessageHandler,\n receiveMessageFromBun: defaultUserMessageHandler\n };\n} else {\n window.__electrobun.receiveInternalMessageFromHost = internalMessageHandler;\n window.__electrobun.receiveMessageFromHost = defaultUserMessageHandler;\n window.__electrobun.receiveInternalMessageFromBun = internalMessageHandler;\n window.__electrobun.receiveMessageFromBun = defaultUserMessageHandler;\n}\ninitHostMessageBridge();\ninitLifecycleEvents();\ninitCmdClickHandling();\ninitSPANavigationInterception();\ninitOverscrollPrevention();\ninitDragRegions();\ninitExternalDropFocusRestoration();\ninitWebviewTag();\ninitWgpuTag();\n})();";
6
6
 
7
7
  // Minimal preload for sandboxed/untrusted webviews (lifecycle events only, no RPC)
8
- export const preloadScriptSandboxed = "(function(){// src/preload/events.ts\nfunction emitWebviewEvent(eventName, detail) {\n setTimeout(() => {\n const bridge = window.__electrobunEventBridge || window.__electrobunInternalBridge;\n bridge?.postMessage(JSON.stringify({\n id: \"webviewEvent\",\n type: \"message\",\n payload: {\n id: window.__electrobunWebviewId,\n eventName,\n detail\n }\n }));\n });\n}\nfunction initLifecycleEvents() {\n window.addEventListener(\"load\", () => {\n if (window === window.top) {\n emitWebviewEvent(\"dom-ready\", document.location.href);\n }\n });\n window.addEventListener(\"popstate\", () => {\n emitWebviewEvent(\"did-navigate-in-page\", window.location.href);\n });\n window.addEventListener(\"hashchange\", () => {\n emitWebviewEvent(\"did-navigate-in-page\", window.location.href);\n });\n}\nvar cmdKeyHeld = false;\nvar cmdKeyTimestamp = 0;\nvar CMD_KEY_THRESHOLD_MS = 500;\nfunction isCmdHeld() {\n if (cmdKeyHeld)\n return true;\n return Date.now() - cmdKeyTimestamp < CMD_KEY_THRESHOLD_MS && cmdKeyTimestamp > 0;\n}\nfunction initCmdClickHandling() {\n window.addEventListener(\"keydown\", (event) => {\n if (event.key === \"Meta\" || event.metaKey) {\n cmdKeyHeld = true;\n cmdKeyTimestamp = Date.now();\n }\n }, true);\n window.addEventListener(\"keyup\", (event) => {\n if (event.key === \"Meta\") {\n cmdKeyHeld = false;\n cmdKeyTimestamp = Date.now();\n }\n }, true);\n window.addEventListener(\"blur\", () => {\n cmdKeyHeld = false;\n });\n window.addEventListener(\"click\", (event) => {\n if (event.metaKey || event.ctrlKey) {\n const anchor = event.target?.closest?.(\"a\");\n if (anchor && anchor.href) {\n event.preventDefault();\n event.stopPropagation();\n event.stopImmediatePropagation();\n emitWebviewEvent(\"new-window-open\", JSON.stringify({\n url: anchor.href,\n isCmdClick: true,\n isSPANavigation: false\n }));\n }\n }\n }, true);\n}\nfunction initSPANavigationInterception() {\n const originalPushState = history.pushState;\n const originalReplaceState = history.replaceState;\n history.pushState = function(state, title, url) {\n if (isCmdHeld() && url) {\n const resolvedUrl = new URL(String(url), window.location.href).href;\n emitWebviewEvent(\"new-window-open\", JSON.stringify({\n url: resolvedUrl,\n isCmdClick: true,\n isSPANavigation: true\n }));\n return;\n }\n return originalPushState.apply(this, [state, title, url]);\n };\n history.replaceState = function(state, title, url) {\n if (isCmdHeld() && url) {\n const resolvedUrl = new URL(String(url), window.location.href).href;\n emitWebviewEvent(\"new-window-open\", JSON.stringify({\n url: resolvedUrl,\n isCmdClick: true,\n isSPANavigation: true\n }));\n return;\n }\n return originalReplaceState.apply(this, [state, title, url]);\n };\n}\nfunction shouldApplyOverscrollPrevention(platform) {\n return platform !== \"linux\";\n}\nfunction initOverscrollPrevention(targetDocument = document, platform = window.__electrobunPlatform) {\n if (!shouldApplyOverscrollPrevention(platform))\n return;\n targetDocument.addEventListener(\"DOMContentLoaded\", () => {\n const style = targetDocument.createElement(\"style\");\n style.type = \"text/css\";\n style.appendChild(targetDocument.createTextNode(\"html, body { overscroll-behavior: none; }\"));\n targetDocument.head.appendChild(style);\n });\n}\n\n// src/preload/index-sandboxed.ts\ninitLifecycleEvents();\ninitCmdClickHandling();\ninitSPANavigationInterception();\ninitOverscrollPrevention();\n})();";
8
+ export const preloadScriptSandboxed = "(function(){// src/preload/events.ts\nfunction emitWebviewEvent(eventName, detail) {\n setTimeout(() => {\n const bridge = window.__electrobunEventBridge || window.__electrobunInternalBridge;\n bridge?.postMessage(JSON.stringify({\n id: \"webviewEvent\",\n type: \"message\",\n payload: {\n id: window.__electrobunWebviewId,\n eventName,\n detail\n }\n }));\n });\n}\nfunction initHostMessageBridge(targetWindow = window, emit = emitWebviewEvent) {\n targetWindow.__electrobunSendToHost = (message) => {\n emit(\"host-message\", JSON.stringify(message));\n };\n}\nfunction initLifecycleEvents() {\n window.addEventListener(\"load\", () => {\n if (window === window.top) {\n emitWebviewEvent(\"dom-ready\", document.location.href);\n }\n });\n window.addEventListener(\"popstate\", () => {\n emitWebviewEvent(\"did-navigate-in-page\", window.location.href);\n });\n window.addEventListener(\"hashchange\", () => {\n emitWebviewEvent(\"did-navigate-in-page\", window.location.href);\n });\n}\nvar cmdKeyHeld = false;\nvar cmdKeyTimestamp = 0;\nvar CMD_KEY_THRESHOLD_MS = 500;\nfunction isCmdHeld() {\n if (cmdKeyHeld)\n return true;\n return Date.now() - cmdKeyTimestamp < CMD_KEY_THRESHOLD_MS && cmdKeyTimestamp > 0;\n}\nfunction initCmdClickHandling() {\n window.addEventListener(\"keydown\", (event) => {\n if (event.key === \"Meta\" || event.metaKey) {\n cmdKeyHeld = true;\n cmdKeyTimestamp = Date.now();\n }\n }, true);\n window.addEventListener(\"keyup\", (event) => {\n if (event.key === \"Meta\") {\n cmdKeyHeld = false;\n cmdKeyTimestamp = Date.now();\n }\n }, true);\n window.addEventListener(\"blur\", () => {\n cmdKeyHeld = false;\n });\n window.addEventListener(\"click\", (event) => {\n if (event.metaKey || event.ctrlKey) {\n const anchor = event.target?.closest?.(\"a\");\n if (anchor && anchor.href) {\n event.preventDefault();\n event.stopPropagation();\n event.stopImmediatePropagation();\n emitWebviewEvent(\"new-window-open\", JSON.stringify({\n url: anchor.href,\n isCmdClick: true,\n isSPANavigation: false\n }));\n }\n }\n }, true);\n}\nfunction initSPANavigationInterception() {\n const originalPushState = history.pushState;\n const originalReplaceState = history.replaceState;\n history.pushState = function(state, title, url) {\n if (isCmdHeld() && url) {\n const resolvedUrl = new URL(String(url), window.location.href).href;\n emitWebviewEvent(\"new-window-open\", JSON.stringify({\n url: resolvedUrl,\n isCmdClick: true,\n isSPANavigation: true\n }));\n return;\n }\n return originalPushState.apply(this, [state, title, url]);\n };\n history.replaceState = function(state, title, url) {\n if (isCmdHeld() && url) {\n const resolvedUrl = new URL(String(url), window.location.href).href;\n emitWebviewEvent(\"new-window-open\", JSON.stringify({\n url: resolvedUrl,\n isCmdClick: true,\n isSPANavigation: true\n }));\n return;\n }\n return originalReplaceState.apply(this, [state, title, url]);\n };\n}\nfunction shouldApplyOverscrollPrevention(platform) {\n return platform !== \"linux\";\n}\nfunction initOverscrollPrevention(targetDocument = document, platform = window.__electrobunPlatform) {\n if (!shouldApplyOverscrollPrevention(platform))\n return;\n targetDocument.addEventListener(\"DOMContentLoaded\", () => {\n const style = targetDocument.createElement(\"style\");\n style.type = \"text/css\";\n style.appendChild(targetDocument.createTextNode(\"html, body { overscroll-behavior: none; }\"));\n targetDocument.head.appendChild(style);\n });\n}\n\n// src/preload/index-sandboxed.ts\ninitHostMessageBridge();\ninitLifecycleEvents();\ninitCmdClickHandling();\ninitSPANavigationInterception();\ninitOverscrollPrevention();\n})();";
@@ -1,9 +1,29 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import {
3
+ initHostMessageBridge,
3
4
  initOverscrollPrevention,
4
5
  shouldApplyOverscrollPrevention,
5
6
  } from "./events";
6
7
 
8
+ describe("host message bridge", () => {
9
+ test("serializes messages onto the event-only bridge", () => {
10
+ const emitted: Array<{ eventName: string; detail: string }> = [];
11
+ const targetWindow = {} as Window;
12
+
13
+ initHostMessageBridge(targetWindow, (eventName, detail) => {
14
+ emitted.push({ eventName, detail });
15
+ });
16
+ targetWindow.__electrobunSendToHost({ type: "ready", count: 2 });
17
+
18
+ expect(emitted).toEqual([
19
+ {
20
+ eventName: "host-message",
21
+ detail: '{"type":"ready","count":2}',
22
+ },
23
+ ]);
24
+ });
25
+ });
26
+
7
27
  describe("overscroll prevention", () => {
8
28
  test("does not install the root overscroll rule on Linux", () => {
9
29
  let listenerRegistered = false;
@@ -28,6 +28,15 @@ export function emitWebviewEvent(eventName: string, detail: string) {
28
28
  });
29
29
  }
30
30
 
31
+ export function initHostMessageBridge(
32
+ targetWindow: Window = window,
33
+ emit: typeof emitWebviewEvent = emitWebviewEvent,
34
+ ) {
35
+ targetWindow.__electrobunSendToHost = (message: unknown) => {
36
+ emit("host-message", JSON.stringify(message));
37
+ };
38
+ }
39
+
31
40
  // Set up standard lifecycle event listeners
32
41
  export function initLifecycleEvents() {
33
42
  // Emit dom-ready when page loads (top-level window only)
@@ -11,6 +11,7 @@
11
11
 
12
12
  import "./globals.d.ts";
13
13
  import {
14
+ initHostMessageBridge,
14
15
  initLifecycleEvents,
15
16
  initCmdClickHandling,
16
17
  initSPANavigationInterception,
@@ -22,6 +23,7 @@ import {
22
23
  // No drag regions - sandboxed content shouldn't control window movement
23
24
  // No webview tags - sandboxed content cannot create OOPIFs
24
25
 
26
+ initHostMessageBridge();
25
27
  initLifecycleEvents();
26
28
  initCmdClickHandling();
27
29
  initSPANavigationInterception();
@@ -22,7 +22,7 @@ import { initExternalDropFocusRestoration } from "./externalDropFocus";
22
22
  import { initWebviewTag } from "./webviewTag";
23
23
  import { initWgpuTag } from "./wgpuTag";
24
24
  import {
25
- emitWebviewEvent,
25
+ initHostMessageBridge,
26
26
  initLifecycleEvents,
27
27
  initCmdClickHandling,
28
28
  initSPANavigationInterception,
@@ -63,12 +63,8 @@ if (!window.__electrobun) {
63
63
  window.__electrobun.receiveMessageFromBun = defaultUserMessageHandler;
64
64
  }
65
65
 
66
- // Allow preload scripts to send custom messages to the host webview
67
- window.__electrobunSendToHost = (message: unknown) => {
68
- emitWebviewEvent("host-message", JSON.stringify(message));
69
- };
70
-
71
66
  // Initialize all features
67
+ initHostMessageBridge();
72
68
  initLifecycleEvents();
73
69
  initCmdClickHandling();
74
70
  initSPANavigationInterception();
@@ -12,8 +12,6 @@ export type BuildConfigType = {
12
12
  channel: string;
13
13
  /** False for dev builds; true for canary, production, and other release channels. */
14
14
  isPackaged: boolean;
15
- cefVersion?: string;
16
- bunVersion?: string;
17
15
  runtime?: {
18
16
  exitOnLastWindowClosed?: boolean;
19
17
  [key: string]: unknown;
@@ -16,7 +16,7 @@ function between(source: string, start: string, end: string): string {
16
16
  describe("bridge callback payload ownership", () => {
17
17
  it("copies asynchronous payloads in core and releases them explicitly", () => {
18
18
  const core = read("core/main.zig");
19
- const runtime = read("sdks/bun/proc/native.ts");
19
+ const runtime = read("sdks/main/proc/native.ts");
20
20
 
21
21
  expect(core).toContain("dispatchRuntimePostMessage");
22
22
  expect(core).toContain("releaseRuntimeCallbackPayload");
@@ -19,11 +19,11 @@ describe("Linux tray interaction contract", () => {
19
19
  expect(trayDocs).toContain(
20
20
  "Ayatana AppIndicator backend supports tray menus and their actions",
21
21
  );
22
- expect(trayDocs).toContain(
23
- "does not expose raw primary icon activation",
22
+ expect(trayDocs).toMatch(
23
+ /does not\s+expose raw primary icon activation/,
24
24
  );
25
- expect(trayDocs).toContain(
26
- "Linux apps should use <code>setMenu</code> and handle menu actions",
25
+ expect(trayDocs).toMatch(
26
+ /Linux apps should use `setMenu\(\)` and handle\s+menu actions/,
27
27
  );
28
28
  expect(trayDocs).not.toContain(
29
29
  "Fired when the system tray icon is clicked, or when a menu item is clicked",
@@ -4,6 +4,7 @@ import {
4
4
  mkdirSync,
5
5
  mkdtempSync,
6
6
  readFileSync,
7
+ realpathSync,
7
8
  rmSync,
8
9
  writeFileSync,
9
10
  } from "node:fs";
@@ -24,27 +25,29 @@ const packageRoot = resolve(import.meta.dir, "../..");
24
25
  const manifest = JSON.parse(
25
26
  readFileSync(join(packageRoot, "package.json"), "utf8"),
26
27
  ) as PackageManifest;
27
- const tempRoot = mkdtempSync(join(tmpdir(), "electrobun-package-exports-"));
28
+ const tempRoot = realpathSync(
29
+ mkdtempSync(join(tmpdir(), "electrobun-package-exports-")),
30
+ );
28
31
  const consumerRoot = join(tempRoot, "consumer");
29
32
  const stagedPackageRoot = join(consumerRoot, "node_modules", "electrobun");
30
33
 
31
34
  const focusedImports = {
32
- "electrobun/bun/app-menu": "setApplicationMenu",
33
- "electrobun/bun/browser-view": "BrowserView",
34
- "electrobun/bun/browser-window": "BrowserWindow",
35
- "electrobun/bun/build-config": "BuildConfig",
36
- "electrobun/bun/context-menu": "showContextMenu",
37
- "electrobun/bun/events": "electrobunEventEmitter",
38
- "electrobun/bun/gpu-window": "GpuWindow",
39
- "electrobun/bun/native": "Screen",
40
- "electrobun/bun/paths": "RESOURCES_FOLDER",
41
- "electrobun/bun/rpc": "createRPC",
42
- "electrobun/bun/socket": "sendMessageToWebviewViaSocket",
43
- "electrobun/bun/tray": "Tray",
44
- "electrobun/bun/updater": "Updater",
45
- "electrobun/bun/utils": "openExternal",
46
- "electrobun/bun/webgpu": "webgpu",
47
- "electrobun/bun/wgpu-view": "WGPUView",
35
+ "electrobun/main/app-menu": "setApplicationMenu",
36
+ "electrobun/main/browser-view": "BrowserView",
37
+ "electrobun/main/browser-window": "BrowserWindow",
38
+ "electrobun/main/build-config": "BuildConfig",
39
+ "electrobun/main/context-menu": "showContextMenu",
40
+ "electrobun/main/events": "electrobunEventEmitter",
41
+ "electrobun/main/gpu-window": "GpuWindow",
42
+ "electrobun/main/native": "Screen",
43
+ "electrobun/main/paths": "RESOURCES_FOLDER",
44
+ "electrobun/main/rpc": "createRPC",
45
+ "electrobun/main/socket": "sendMessageToWebviewViaSocket",
46
+ "electrobun/main/tray": "Tray",
47
+ "electrobun/main/updater": "Updater",
48
+ "electrobun/main/utils": "openExternal",
49
+ "electrobun/main/webgpu": "webgpu",
50
+ "electrobun/main/wgpu-view": "WGPUView",
48
51
  "electrobun/rpc": "defineElectrobunRPC",
49
52
  } as const;
50
53
 
@@ -66,7 +69,7 @@ beforeAll(() => {
66
69
  }),
67
70
  );
68
71
 
69
- copyApiDirectory("sdks/bun", join(stagedPackageRoot, "dist/api/sdks/bun"));
72
+ copyApiDirectory("sdks/main", join(stagedPackageRoot, "dist/api/sdks/main"));
70
73
  copyApiDirectory("browser", join(stagedPackageRoot, "dist/api/browser"));
71
74
  copyApiDirectory("shared", join(stagedPackageRoot, "dist/api/shared"));
72
75
  copyApiDirectory("config", join(stagedPackageRoot, "dist/api/config"));
@@ -132,22 +135,31 @@ function reportComparison(
132
135
  }
133
136
 
134
137
  describe("published package exports", () => {
135
- test("preserves the existing root, bun, and view entry points", () => {
136
- expect(manifest.exports["."]).toBe("./dist/api/sdks/bun/index.ts");
137
- expect(manifest.exports["./bun"]).toBe("./dist/api/sdks/bun/index.ts");
138
+ test("publishes the canonical main SDK and preserves legacy entry points", () => {
139
+ expect(manifest.exports["."]).toBe("./dist/api/sdks/main/index.ts");
140
+ expect(manifest.exports["./main"]).toBe("./dist/api/sdks/main/index.ts");
141
+ expect(manifest.exports["./bun"]).toBe("./dist/api/sdks/main/index.ts");
138
142
  expect(manifest.exports["./view"]).toBe("./dist/api/browser/index.ts");
139
143
  });
140
144
 
145
+ test("maps every deprecated bun subpath to its canonical main target", () => {
146
+ for (const exportKey of Object.keys(manifest.exports)) {
147
+ if (!exportKey.startsWith("./main/")) continue;
148
+ const legacyKey = exportKey.replace("./main/", "./bun/");
149
+ expect(manifest.exports[legacyKey]).toBe(manifest.exports[exportKey]);
150
+ }
151
+ });
152
+
141
153
  test("maps every focused entry to a published, re-export-only module", () => {
142
154
  for (const specifier of Object.keys(focusedImports)) {
143
155
  const exportKey = `.${specifier.slice("electrobun".length)}`;
144
156
  const target = manifest.exports[exportKey];
145
157
  if (!target) throw new Error(`Missing package export ${exportKey}`);
146
- expect(target).toMatch(/^\.\/dist\/api\/sdks\/bun\/entries\/.+\.ts$/);
158
+ expect(target).toMatch(/^\.\/dist\/api\/sdks\/main\/entries\/.+\.ts$/);
147
159
 
148
160
  const sourcePath = join(
149
161
  packageRoot,
150
- target.replace("./dist/api/sdks/bun", "src/sdks/bun"),
162
+ target.replace("./dist/api/sdks/main", "src/sdks/main"),
151
163
  );
152
164
  const source = readFileSync(sourcePath, "utf8");
153
165
  expect(source).not.toMatch(/^\s*import\s/m);
@@ -161,19 +173,35 @@ describe("published package exports", () => {
161
173
  ).entries()) {
162
174
  const graph = await bundleImport(`focused-${index}`, specifier, exportName);
163
175
  expect(graph.modules).toBeGreaterThan(1);
164
- expect(graph.inputs).not.toContain("dist/api/sdks/bun/index.ts");
176
+ expect(graph.inputs).not.toContain("dist/api/sdks/main/index.ts");
165
177
  }
166
178
  });
167
179
 
180
+ test("deprecated bun imports resolve through the canonical SDK", async () => {
181
+ const root = await bundleImport(
182
+ "legacy-bun-root",
183
+ "electrobun/bun",
184
+ "BrowserWindow",
185
+ );
186
+ const rpc = await bundleImport(
187
+ "legacy-bun-rpc",
188
+ "electrobun/bun/rpc",
189
+ "createRPC",
190
+ );
191
+
192
+ expect(root.inputs).toContain("dist/api/sdks/main/index.ts");
193
+ expect(rpc.inputs).not.toContain("dist/api/sdks/main/index.ts");
194
+ });
195
+
168
196
  test("pure RPC entry excludes the native and main-process graph", async () => {
169
197
  const root = await bundleImport("root-rpc", "electrobun", "createRPC");
170
198
  const rpc = await bundleImport(
171
199
  "focused-rpc",
172
- "electrobun/bun/rpc",
200
+ "electrobun/main/rpc",
173
201
  "createRPC",
174
202
  );
175
203
 
176
- expect(rpc.inputs).not.toContain("dist/api/sdks/bun/proc/native.ts");
204
+ expect(rpc.inputs).not.toContain("dist/api/sdks/main/proc/native.ts");
177
205
  expect(rpc.inputs.some((input) => input.includes("/core/"))).toBe(false);
178
206
  expect(rpc.modules).toBeLessThan(root.modules);
179
207
  expect(rpc.bytes).toBeLessThan(root.bytes);
@@ -184,19 +212,19 @@ describe("published package exports", () => {
184
212
  const root = await bundleImport("root-window", "electrobun", "BrowserWindow");
185
213
  const browserWindow = await bundleImport(
186
214
  "focused-window",
187
- "electrobun/bun/browser-window",
215
+ "electrobun/main/browser-window",
188
216
  "BrowserWindow",
189
217
  );
190
218
  const appMenu = await bundleImport(
191
219
  "focused-menu",
192
- "electrobun/bun/app-menu",
220
+ "electrobun/main/app-menu",
193
221
  "setApplicationMenu",
194
222
  );
195
223
 
196
224
  for (const graph of [browserWindow, appMenu]) {
197
- expect(graph.inputs).not.toContain("dist/api/sdks/bun/core/Updater.ts");
198
- expect(graph.inputs).not.toContain("dist/api/sdks/bun/webGPU.ts");
199
- expect(graph.inputs).not.toContain("dist/api/sdks/bun/webgpuAdapter.ts");
225
+ expect(graph.inputs).not.toContain("dist/api/sdks/main/core/Updater.ts");
226
+ expect(graph.inputs).not.toContain("dist/api/sdks/main/webGPU.ts");
227
+ expect(graph.inputs).not.toContain("dist/api/sdks/main/webgpuAdapter.ts");
200
228
  expect(graph.modules).toBeLessThan(root.modules);
201
229
  expect(graph.bytes).toBeLessThan(root.bytes);
202
230
  }
@@ -55,8 +55,8 @@ describe("published package dependency contract", () => {
55
55
  'export const preloadScript = "";\nexport const preloadScriptSandboxed = "";\n',
56
56
  );
57
57
  cpSync(
58
- join(packageDir, "src", "sdks", "bun"),
59
- join(publishedApi, "sdks", "bun"),
58
+ join(packageDir, "src", "sdks", "main"),
59
+ join(publishedApi, "sdks", "main"),
60
60
  { recursive: true },
61
61
  );
62
62
 
@@ -73,6 +73,11 @@ describe("published package dependency contract", () => {
73
73
  consumerPath,
74
74
  [
75
75
  'import { three, type ElectrobunConfig } from "electrobun";',
76
+ 'import type { BrowserWindow as MainBrowserWindow } from "electrobun/main";',
77
+ 'import type { BrowserWindow as LegacyBrowserWindow } from "electrobun/bun";',
78
+ "declare const mainWindow: MainBrowserWindow;",
79
+ "const legacyWindow: LegacyBrowserWindow = mainWindow;",
80
+ "void legacyWindow;",
76
81
  "const scene: three.Scene = new three.Scene();",
77
82
  "scene.add(new three.Object3D());",
78
83
  "const config = {",
@@ -120,7 +125,7 @@ describe("published package dependency contract", () => {
120
125
 
121
126
  if (result.error) throw result.error;
122
127
  expect([result.status, result.stdout, result.stderr]).toEqual([0, "", ""]);
123
- expect(manifest.exports["."]).toBe("./dist/api/sdks/bun/index.ts");
128
+ expect(manifest.exports["."]).toBe("./dist/api/sdks/main/index.ts");
124
129
  } finally {
125
130
  rmSync(tempDir, { force: true, recursive: true });
126
131
  }
@@ -37,8 +37,8 @@ test("CEF and non-macOS native renderers report unsupported", () => {
37
37
 
38
38
  test("every main-process SDK exposes initial and runtime controls", () => {
39
39
  const files = [
40
- "sdks/bun/core/BrowserView.ts",
41
- "sdks/bun/core/BrowserWindow.ts",
40
+ "sdks/main/core/BrowserView.ts",
41
+ "sdks/main/core/BrowserWindow.ts",
42
42
  "sdks/zig/electrobun.zig",
43
43
  "sdks/rust/electrobun.rs",
44
44
  "sdks/go/electrobun.go",
@@ -23,8 +23,8 @@ describe("native window visibility contract", () => {
23
23
 
24
24
  it("exports one core ABI symbol and loads it in every SDK", () => {
25
25
  const core = read("core/main.zig");
26
- const bunWindow = read("sdks/bun/core/BrowserWindow.ts");
27
- const bunNative = read("sdks/bun/proc/native.ts");
26
+ const bunWindow = read("sdks/main/core/BrowserWindow.ts");
27
+ const bunNative = read("sdks/main/proc/native.ts");
28
28
  const zig = read("sdks/zig/electrobun.zig");
29
29
  const rust = read("sdks/rust/electrobun.rs");
30
30
  const go = read("sdks/go/electrobun.go");
@@ -7,7 +7,7 @@ const nativeWrapper = readFileSync(
7
7
  "utf8",
8
8
  );
9
9
  const browserView = readFileSync(
10
- join(import.meta.dirname, "../sdks/bun/core/BrowserView.ts"),
10
+ join(import.meta.dirname, "../sdks/main/core/BrowserView.ts"),
11
11
  "utf8",
12
12
  );
13
13
  const packageBuild = readFileSync(
@@ -1241,6 +1241,11 @@ function emitWebviewEvent(eventName, detail) {
1241
1241
  }));
1242
1242
  });
1243
1243
  }
1244
+ function initHostMessageBridge(targetWindow = window, emit = emitWebviewEvent) {
1245
+ targetWindow.__electrobunSendToHost = (message) => {
1246
+ emit("host-message", JSON.stringify(message));
1247
+ };
1248
+ }
1244
1249
  function initLifecycleEvents() {
1245
1250
  window.addEventListener("load", () => {
1246
1251
  if (window === window.top) {
@@ -1360,9 +1365,7 @@ if (!window.__electrobun) {
1360
1365
  window.__electrobun.receiveInternalMessageFromBun = internalMessageHandler;
1361
1366
  window.__electrobun.receiveMessageFromBun = defaultUserMessageHandler;
1362
1367
  }
1363
- window.__electrobunSendToHost = (message) => {
1364
- emitWebviewEvent("host-message", JSON.stringify(message));
1365
- };
1368
+ initHostMessageBridge();
1366
1369
  initLifecycleEvents();
1367
1370
  initCmdClickHandling();
1368
1371
  initSPANavigationInterception();
@@ -13,6 +13,11 @@ function emitWebviewEvent(eventName, detail) {
13
13
  }));
14
14
  });
15
15
  }
16
+ function initHostMessageBridge(targetWindow = window, emit = emitWebviewEvent) {
17
+ targetWindow.__electrobunSendToHost = (message) => {
18
+ emit("host-message", JSON.stringify(message));
19
+ };
20
+ }
16
21
  function initLifecycleEvents() {
17
22
  window.addEventListener("load", () => {
18
23
  if (window === window.top) {
@@ -109,6 +114,7 @@ function initOverscrollPrevention(targetDocument = document, platform = window._
109
114
  }
110
115
 
111
116
  // src/preload/index-sandboxed.ts
117
+ initHostMessageBridge();
112
118
  initLifecycleEvents();
113
119
  initCmdClickHandling();
114
120
  initSPANavigationInterception();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "electrobun",
3
- "version": "1.18.4-beta.17",
3
+ "version": "1.18.4-beta.18",
4
4
  "description": "Build ultra fast, tiny, and cross-platform desktop apps with Typescript.",
5
5
  "license": "MIT",
6
6
  "author": "Blackboard Technologies Inc.",
@@ -12,25 +12,42 @@
12
12
  "typescript"
13
13
  ],
14
14
  "exports": {
15
- ".": "./dist/api/sdks/bun/index.ts",
16
- "./bun": "./dist/api/sdks/bun/index.ts",
17
- "./bun/app-menu": "./dist/api/sdks/bun/entries/app-menu.ts",
18
- "./bun/browser-view": "./dist/api/sdks/bun/entries/browser-view.ts",
19
- "./bun/browser-window": "./dist/api/sdks/bun/entries/browser-window.ts",
20
- "./bun/build-config": "./dist/api/sdks/bun/entries/build-config.ts",
21
- "./bun/context-menu": "./dist/api/sdks/bun/entries/context-menu.ts",
22
- "./bun/events": "./dist/api/sdks/bun/entries/events.ts",
23
- "./bun/gpu-window": "./dist/api/sdks/bun/entries/gpu-window.ts",
24
- "./bun/native": "./dist/api/sdks/bun/entries/native.ts",
25
- "./bun/paths": "./dist/api/sdks/bun/entries/paths.ts",
26
- "./bun/rpc": "./dist/api/sdks/bun/entries/rpc.ts",
27
- "./bun/socket": "./dist/api/sdks/bun/entries/socket.ts",
28
- "./bun/tray": "./dist/api/sdks/bun/entries/tray.ts",
29
- "./bun/updater": "./dist/api/sdks/bun/entries/updater.ts",
30
- "./bun/utils": "./dist/api/sdks/bun/entries/utils.ts",
31
- "./bun/webgpu": "./dist/api/sdks/bun/entries/webgpu.ts",
32
- "./bun/wgpu-view": "./dist/api/sdks/bun/entries/wgpu-view.ts",
33
- "./rpc": "./dist/api/sdks/bun/entries/rpc.ts",
15
+ ".": "./dist/api/sdks/main/index.ts",
16
+ "./main": "./dist/api/sdks/main/index.ts",
17
+ "./main/app-menu": "./dist/api/sdks/main/entries/app-menu.ts",
18
+ "./main/browser-view": "./dist/api/sdks/main/entries/browser-view.ts",
19
+ "./main/browser-window": "./dist/api/sdks/main/entries/browser-window.ts",
20
+ "./main/build-config": "./dist/api/sdks/main/entries/build-config.ts",
21
+ "./main/context-menu": "./dist/api/sdks/main/entries/context-menu.ts",
22
+ "./main/events": "./dist/api/sdks/main/entries/events.ts",
23
+ "./main/gpu-window": "./dist/api/sdks/main/entries/gpu-window.ts",
24
+ "./main/native": "./dist/api/sdks/main/entries/native.ts",
25
+ "./main/paths": "./dist/api/sdks/main/entries/paths.ts",
26
+ "./main/rpc": "./dist/api/sdks/main/entries/rpc.ts",
27
+ "./main/socket": "./dist/api/sdks/main/entries/socket.ts",
28
+ "./main/tray": "./dist/api/sdks/main/entries/tray.ts",
29
+ "./main/updater": "./dist/api/sdks/main/entries/updater.ts",
30
+ "./main/utils": "./dist/api/sdks/main/entries/utils.ts",
31
+ "./main/webgpu": "./dist/api/sdks/main/entries/webgpu.ts",
32
+ "./main/wgpu-view": "./dist/api/sdks/main/entries/wgpu-view.ts",
33
+ "./bun": "./dist/api/sdks/main/index.ts",
34
+ "./bun/app-menu": "./dist/api/sdks/main/entries/app-menu.ts",
35
+ "./bun/browser-view": "./dist/api/sdks/main/entries/browser-view.ts",
36
+ "./bun/browser-window": "./dist/api/sdks/main/entries/browser-window.ts",
37
+ "./bun/build-config": "./dist/api/sdks/main/entries/build-config.ts",
38
+ "./bun/context-menu": "./dist/api/sdks/main/entries/context-menu.ts",
39
+ "./bun/events": "./dist/api/sdks/main/entries/events.ts",
40
+ "./bun/gpu-window": "./dist/api/sdks/main/entries/gpu-window.ts",
41
+ "./bun/native": "./dist/api/sdks/main/entries/native.ts",
42
+ "./bun/paths": "./dist/api/sdks/main/entries/paths.ts",
43
+ "./bun/rpc": "./dist/api/sdks/main/entries/rpc.ts",
44
+ "./bun/socket": "./dist/api/sdks/main/entries/socket.ts",
45
+ "./bun/tray": "./dist/api/sdks/main/entries/tray.ts",
46
+ "./bun/updater": "./dist/api/sdks/main/entries/updater.ts",
47
+ "./bun/utils": "./dist/api/sdks/main/entries/utils.ts",
48
+ "./bun/webgpu": "./dist/api/sdks/main/entries/webgpu.ts",
49
+ "./bun/wgpu-view": "./dist/api/sdks/main/entries/wgpu-view.ts",
50
+ "./rpc": "./dist/api/sdks/main/entries/rpc.ts",
34
51
  "./view": "./dist/api/browser/index.ts"
35
52
  },
36
53
  "type": "module",
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes