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.
- package/README.md +14 -9
- package/dash.config.ts +3 -3
- package/dist/api/browser/global.d.ts +2 -0
- package/dist/api/browser/webviewtag.ts +14 -7
- package/dist/api/browser/wgputag.ts +1 -1
- package/dist/api/config/ElectrobunConfig.ts +11 -49
- package/dist/api/preload/.generated/compiled.ts +2 -2
- package/dist/api/preload/events.test.ts +20 -0
- package/dist/api/preload/events.ts +9 -0
- package/dist/api/preload/index-sandboxed.ts +2 -0
- package/dist/api/preload/index.ts +2 -6
- package/dist/api/sdks/{bun → main}/core/BuildConfig.ts +0 -2
- package/dist/api/shared/bridge-payload-ownership.test.ts +1 -1
- package/dist/api/shared/linux-tray-contract.test.ts +4 -4
- package/dist/api/shared/package-exports.test.ts +59 -31
- package/dist/api/shared/published-package-contract.test.ts +8 -3
- package/dist/api/shared/spell-check.test.js +2 -2
- package/dist/api/shared/window-visibility.test.ts +2 -2
- package/dist/api/shared/windows-unicode-ui.test.ts +1 -1
- package/dist/preload-full.js +6 -3
- package/dist/preload-sandboxed.js +6 -0
- package/package.json +37 -20
- /package/dist/api/sdks/{bun → main}/__tests__/build-config.test.ts +0 -0
- /package/dist/api/sdks/{bun → main}/__tests__/ffi-contract.test.ts +0 -0
- /package/dist/api/sdks/{bun → main}/core/ApplicationMenu.ts +0 -0
- /package/dist/api/sdks/{bun → main}/core/BrowserView.ts +0 -0
- /package/dist/api/sdks/{bun → main}/core/BrowserWindow.ts +0 -0
- /package/dist/api/sdks/{bun → main}/core/ContextMenu.ts +0 -0
- /package/dist/api/sdks/{bun → main}/core/GpuWindow.ts +0 -0
- /package/dist/api/sdks/{bun → main}/core/Paths.ts +0 -0
- /package/dist/api/sdks/{bun → main}/core/Socket.ts +0 -0
- /package/dist/api/sdks/{bun → main}/core/Tray.ts +0 -0
- /package/dist/api/sdks/{bun → main}/core/Updater.ts +0 -0
- /package/dist/api/sdks/{bun → main}/core/Utils.ts +0 -0
- /package/dist/api/sdks/{bun → main}/core/WGPUView.ts +0 -0
- /package/dist/api/sdks/{bun → main}/core/WindowsUpdateTask.test.ts +0 -0
- /package/dist/api/sdks/{bun → main}/core/WindowsUpdateTask.ts +0 -0
- /package/dist/api/sdks/{bun → main}/core/menuRoles.ts +0 -0
- /package/dist/api/sdks/{bun → main}/entries/app-menu.ts +0 -0
- /package/dist/api/sdks/{bun → main}/entries/browser-view.ts +0 -0
- /package/dist/api/sdks/{bun → main}/entries/browser-window.ts +0 -0
- /package/dist/api/sdks/{bun → main}/entries/build-config.ts +0 -0
- /package/dist/api/sdks/{bun → main}/entries/context-menu.ts +0 -0
- /package/dist/api/sdks/{bun → main}/entries/events.ts +0 -0
- /package/dist/api/sdks/{bun → main}/entries/gpu-window.ts +0 -0
- /package/dist/api/sdks/{bun → main}/entries/native.ts +0 -0
- /package/dist/api/sdks/{bun → main}/entries/paths.ts +0 -0
- /package/dist/api/sdks/{bun → main}/entries/rpc.ts +0 -0
- /package/dist/api/sdks/{bun → main}/entries/socket.ts +0 -0
- /package/dist/api/sdks/{bun → main}/entries/tray.ts +0 -0
- /package/dist/api/sdks/{bun → main}/entries/updater.ts +0 -0
- /package/dist/api/sdks/{bun → main}/entries/utils.ts +0 -0
- /package/dist/api/sdks/{bun → main}/entries/webgpu.ts +0 -0
- /package/dist/api/sdks/{bun → main}/entries/wgpu-view.ts +0 -0
- /package/dist/api/sdks/{bun → main}/events/ApplicationEvents.ts +0 -0
- /package/dist/api/sdks/{bun → main}/events/event.ts +0 -0
- /package/dist/api/sdks/{bun → main}/events/eventEmitter.ts +0 -0
- /package/dist/api/sdks/{bun → main}/events/trayEvents.ts +0 -0
- /package/dist/api/sdks/{bun → main}/events/webviewEvents.ts +0 -0
- /package/dist/api/sdks/{bun → main}/events/windowEvents.ts +0 -0
- /package/dist/api/sdks/{bun → main}/index.ts +0 -0
- /package/dist/api/sdks/{bun → main}/proc/eventBridge.test.ts +0 -0
- /package/dist/api/sdks/{bun → main}/proc/eventBridge.ts +0 -0
- /package/dist/api/sdks/{bun → main}/proc/linux.md +0 -0
- /package/dist/api/sdks/{bun → main}/proc/native.ts +0 -0
- /package/dist/api/sdks/{bun → main}/webGPU.ts +0 -0
- /package/dist/api/sdks/{bun → main}/webgpuAdapter.ts +0 -0
- /package/dist/api/sdks/{bun → main}/webgpuRenderPass.test.ts +0 -0
- /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
|
|
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
|
|
17
|
-
|
|
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
|
-
|
|
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
|
|
25
|
-
-
|
|
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-
|
|
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
|
|
37
|
-
-
|
|
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.
|
|
1
|
+
// @dash cli=0.5.0-canary.8 cottontail=0.2.3
|
|
2
2
|
export default {
|
|
3
3
|
scripts: {
|
|
4
|
-
start: "hutch src/sdks/
|
|
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/
|
|
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
|
|
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
|
-
|
|
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
|
|
53
|
-
togglePassthrough(enablePassthrough?: boolean
|
|
54
|
-
toggleHidden(hidden?: boolean
|
|
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;
|
|
@@ -8,12 +8,14 @@ import type { WindowsWebView2Permission } from "./windowsPermissions";
|
|
|
8
8
|
type BundlerOptions = {
|
|
9
9
|
external?: string[];
|
|
10
10
|
define?: Record<string, string>;
|
|
11
|
-
|
|
11
|
+
alias?: Record<string, string>;
|
|
12
12
|
minify?: boolean;
|
|
13
13
|
sourcemap?: boolean | "inline" | "external" | "linked";
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
232
|
-
*
|
|
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
|
-
} &
|
|
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
|
|
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
|
-
|
|
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/
|
|
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).
|
|
23
|
-
|
|
22
|
+
expect(trayDocs).toMatch(
|
|
23
|
+
/does not\s+expose raw primary icon activation/,
|
|
24
24
|
);
|
|
25
|
-
expect(trayDocs).
|
|
26
|
-
|
|
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 =
|
|
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/
|
|
33
|
-
"electrobun/
|
|
34
|
-
"electrobun/
|
|
35
|
-
"electrobun/
|
|
36
|
-
"electrobun/
|
|
37
|
-
"electrobun/
|
|
38
|
-
"electrobun/
|
|
39
|
-
"electrobun/
|
|
40
|
-
"electrobun/
|
|
41
|
-
"electrobun/
|
|
42
|
-
"electrobun/
|
|
43
|
-
"electrobun/
|
|
44
|
-
"electrobun/
|
|
45
|
-
"electrobun/
|
|
46
|
-
"electrobun/
|
|
47
|
-
"electrobun/
|
|
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/
|
|
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("
|
|
136
|
-
expect(manifest.exports["."]).toBe("./dist/api/sdks/
|
|
137
|
-
expect(manifest.exports["./
|
|
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\/
|
|
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/
|
|
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/
|
|
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/
|
|
200
|
+
"electrobun/main/rpc",
|
|
173
201
|
"createRPC",
|
|
174
202
|
);
|
|
175
203
|
|
|
176
|
-
expect(rpc.inputs).not.toContain("dist/api/sdks/
|
|
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/
|
|
215
|
+
"electrobun/main/browser-window",
|
|
188
216
|
"BrowserWindow",
|
|
189
217
|
);
|
|
190
218
|
const appMenu = await bundleImport(
|
|
191
219
|
"focused-menu",
|
|
192
|
-
"electrobun/
|
|
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/
|
|
198
|
-
expect(graph.inputs).not.toContain("dist/api/sdks/
|
|
199
|
-
expect(graph.inputs).not.toContain("dist/api/sdks/
|
|
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", "
|
|
59
|
-
join(publishedApi, "sdks", "
|
|
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/
|
|
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/
|
|
41
|
-
"sdks/
|
|
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/
|
|
27
|
-
const bunNative = read("sdks/
|
|
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/
|
|
10
|
+
join(import.meta.dirname, "../sdks/main/core/BrowserView.ts"),
|
|
11
11
|
"utf8",
|
|
12
12
|
);
|
|
13
13
|
const packageBuild = readFileSync(
|
package/dist/preload-full.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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/
|
|
16
|
-
"./
|
|
17
|
-
"./
|
|
18
|
-
"./
|
|
19
|
-
"./
|
|
20
|
-
"./
|
|
21
|
-
"./
|
|
22
|
-
"./
|
|
23
|
-
"./
|
|
24
|
-
"./
|
|
25
|
-
"./
|
|
26
|
-
"./
|
|
27
|
-
"./
|
|
28
|
-
"./
|
|
29
|
-
"./
|
|
30
|
-
"./
|
|
31
|
-
"./
|
|
32
|
-
"./
|
|
33
|
-
"./
|
|
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
|
|
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
|