electrobun 1.18.1-beta.1 → 1.18.4-beta.10
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 +37 -14
- package/dash.config.ts +28 -0
- package/dist/api/browser/global.d.ts +6 -0
- package/dist/api/browser/index.ts +162 -44
- package/dist/api/{bun → config}/ElectrobunConfig.ts +109 -40
- package/dist/api/config/validate.test.ts +31 -0
- package/dist/api/config/validate.ts +19 -0
- package/dist/api/preload/.generated/compiled.ts +8 -0
- package/dist/api/{bun/preload → preload}/build.ts +6 -6
- package/dist/api/{bun/preload → preload}/encryption.ts +11 -5
- package/dist/api/{bun/preload → preload}/globals.d.ts +8 -0
- package/dist/api/{bun/preload → preload}/index.ts +18 -8
- package/dist/api/{bun/preload → preload}/webviewTag.ts +32 -3
- package/dist/api/{bun/preload → preload}/wgpuTag.ts +33 -2
- package/dist/api/{bun → sdks/bun}/__tests__/ffi-contract.test.ts +6 -9
- package/dist/api/{bun → sdks/bun}/core/BrowserView.ts +96 -55
- package/dist/api/{bun → sdks/bun}/core/BrowserWindow.ts +20 -30
- package/dist/api/{bun → sdks/bun}/core/BuildConfig.ts +32 -5
- package/dist/api/{bun → sdks/bun}/core/GpuWindow.ts +20 -7
- package/dist/api/sdks/bun/core/Socket.ts +22 -0
- package/dist/api/{bun → sdks/bun}/core/Tray.ts +33 -32
- package/dist/api/{bun → sdks/bun}/core/Updater.ts +10 -10
- package/dist/api/{bun → sdks/bun}/core/Utils.ts +3 -5
- package/dist/api/{bun → sdks/bun}/core/WGPUView.ts +43 -13
- package/dist/api/{bun → sdks/bun}/index.ts +39 -10
- package/dist/api/{bun → sdks/bun}/proc/native.ts +942 -746
- package/dist/api/{bun → sdks/bun}/webGPU.ts +1 -1
- package/dist/api/{bun → sdks/bun}/webgpuAdapter.ts +40 -33
- package/dist/api/shared/build-dependencies.test.ts +48 -0
- package/dist/api/shared/build-dependencies.ts +46 -0
- package/dist/api/shared/go-version.ts +3 -0
- package/dist/api/shared/odin-version.ts +6 -0
- package/dist/api/shared/rust-version.ts +3 -0
- package/dist/go-sdk/callbacks.go +260 -0
- package/dist/go-sdk/electrobun.go +2021 -0
- package/dist/main.js +35 -28
- package/dist/odin-sdk/electrobun/electrobun.odin +2337 -0
- package/dist/preload-full.js +948 -0
- package/dist/preload-sandboxed.js +111 -0
- package/dist/rust-sdk/electrobun.rs +2396 -0
- package/dist/zig-sdk/electrobun.zig +2005 -0
- package/package.json +5 -29
- package/src/cli/index.ts +868 -654
- package/bin/electrobun.cjs +0 -169
- package/dist/api/bun/core/Socket.ts +0 -205
- package/dist/api/bun/core/windowIds.ts +0 -5
- package/dist/api/bun/preload/.generated/compiled.ts +0 -8
- package/dist/api/shared/bun-version.ts +0 -3
- /package/dist/api/{bun/preload → preload}/dragRegions.ts +0 -0
- /package/dist/api/{bun/preload → preload}/events.ts +0 -0
- /package/dist/api/{bun/preload → preload}/index-sandboxed.ts +0 -0
- /package/dist/api/{bun/preload → preload}/internalRpc.ts +0 -0
- /package/dist/api/{bun/preload → preload}/overlaySync.ts +0 -0
- /package/dist/api/{bun → sdks/bun}/core/ApplicationMenu.ts +0 -0
- /package/dist/api/{bun → sdks/bun}/core/ContextMenu.ts +0 -0
- /package/dist/api/{bun → sdks/bun}/core/Paths.ts +0 -0
- /package/dist/api/{bun → sdks/bun}/core/menuRoles.ts +0 -0
- /package/dist/api/{bun → sdks/bun}/events/ApplicationEvents.ts +0 -0
- /package/dist/api/{bun → sdks/bun}/events/event.ts +0 -0
- /package/dist/api/{bun → sdks/bun}/events/eventEmitter.ts +0 -0
- /package/dist/api/{bun → sdks/bun}/events/trayEvents.ts +0 -0
- /package/dist/api/{bun → sdks/bun}/events/webviewEvents.ts +0 -0
- /package/dist/api/{bun → sdks/bun}/events/windowEvents.ts +0 -0
- /package/dist/api/{bun → sdks/bun}/proc/linux.md +0 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const LEGACY_BUN_CONFIG_ERROR =
|
|
2
|
+
'Bun main-process configuration has been removed. Use build.mainProcess = "cottontail" and move build.bun options to build.cottontail.';
|
|
3
|
+
|
|
4
|
+
export function assertNoLegacyBunMainProcessConfig(config: unknown): void {
|
|
5
|
+
if (!config || typeof config !== "object") return;
|
|
6
|
+
|
|
7
|
+
const build = (config as Record<string, unknown>)["build"];
|
|
8
|
+
if (!build || typeof build !== "object") return;
|
|
9
|
+
|
|
10
|
+
const buildConfig = build as Record<string, unknown>;
|
|
11
|
+
if (
|
|
12
|
+
buildConfig["mainProcess"] === "bun" ||
|
|
13
|
+
Object.hasOwn(buildConfig, "bun") ||
|
|
14
|
+
Object.hasOwn(buildConfig, "bunVersion") ||
|
|
15
|
+
Object.hasOwn(buildConfig, "bunnyBun")
|
|
16
|
+
) {
|
|
17
|
+
throw new Error(LEGACY_BUN_CONFIG_ERROR);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Auto-generated file. Do not edit directly.
|
|
2
|
+
// Run "dash build.ts" or "dash run build:dev" from the package folder to regenerate.
|
|
3
|
+
|
|
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\nfunction isAppRegionDrag(e) {\n const target = e.target;\n if (!target || !target.closest)\n return false;\n if (target.closest(\".electrobun-webkit-app-region-no-drag\") || target.closest('[style*=\"app-region\"][style*=\"no-drag\"]')) {\n return false;\n }\n const draggableByStyle = target.closest('[style*=\"app-region\"][style*=\"drag\"]');\n const draggableByClass = target.closest(\".electrobun-webkit-app-region-drag\");\n return !!(draggableByStyle || draggableByClass);\n}\nfunction initDragRegions() {\n document.addEventListener(\"mousedown\", (e) => {\n if (isAppRegionDrag(e)) {\n send(\"startWindowMove\", { id: window.__electrobunWindowId });\n }\n });\n document.addEventListener(\"mouseup\", (e) => {\n if (isAppRegionDrag(e)) {\n send(\"stopWindowMove\", { id: window.__electrobunWindowId });\n }\n });\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/webviewTag.ts\nvar webviewRegistry = {};\n\nclass ElectrobunWebviewTag extends HTMLElement {\n webviewId = null;\n maskSelectors = new Set;\n _sync = null;\n transparent = false;\n passthroughEnabled = false;\n hidden = false;\n sandboxed = false;\n _eventListeners = {};\n static get observedAttributes() {\n return [\"src\", \"html\"];\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 (newValue === null)\n return;\n if (this.webviewId === null)\n return;\n if (name === \"src\")\n this.loadURL(newValue);\n else if (name === \"html\")\n this.loadHTML(newValue);\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 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 this.transparent = transparent;\n this.passthroughEnabled = passthrough;\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: {\n width: rect.width,\n height: rect.height,\n x: rect.x,\n y: rect.y\n },\n sandbox,\n transparent,\n passthrough,\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 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 loadURL(url) {\n if (this.webviewId === null)\n return;\n this.setAttribute(\"src\", url);\n send(\"webviewTagUpdateSrc\", { id: this.webviewId, url });\n }\n loadHTML(html) {\n if (this.webviewId === null)\n return;\n send(\"webviewTagUpdateHtml\", { id: this.webviewId, 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 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: {\n width: rect.width,\n height: rect.height,\n x: rect.x,\n y: rect.y\n },\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 initOverscrollPrevention() {\n document.addEventListener(\"DOMContentLoaded\", () => {\n const style = document.createElement(\"style\");\n style.type = \"text/css\";\n style.appendChild(document.createTextNode(\"html, body { overscroll-behavior: none; }\"));\n document.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();\ninitWebviewTag();\ninitWgpuTag();\n})();";
|
|
6
|
+
|
|
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 initOverscrollPrevention() {\n document.addEventListener(\"DOMContentLoaded\", () => {\n const style = document.createElement(\"style\");\n style.type = \"text/css\";\n style.appendChild(document.createTextNode(\"html, body { overscroll-behavior: none; }\"));\n document.head.appendChild(style);\n });\n}\n\n// src/preload/index-sandboxed.ts\ninitLifecycleEvents();\ninitCmdClickHandling();\ninitSPANavigationInterception();\ninitOverscrollPrevention();\n})();";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Standalone build script for the preload.
|
|
2
|
-
// Normally this is run as part of "
|
|
3
|
-
//
|
|
2
|
+
// Normally this is run as part of "dash build.ts", but you can run it directly:
|
|
3
|
+
// dash src/preload/build.ts
|
|
4
4
|
|
|
5
5
|
import { join, dirname } from "path";
|
|
6
6
|
import { writeFileSync, mkdirSync } from "fs";
|
|
@@ -40,13 +40,13 @@ async function buildPreload() {
|
|
|
40
40
|
throw new Error("Failed to build sandboxed preload script");
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
//
|
|
44
|
-
// to keep preload globals scoped for
|
|
43
|
+
// The compatible bundler does not currently support IIFE output, so wrap
|
|
44
|
+
// the ESM bundle manually to keep preload globals scoped for injection.
|
|
45
45
|
const fullPreloadJs = `(function(){${await fullResult.outputs[0]!.text()}})();`;
|
|
46
46
|
const sandboxedPreloadJs = `(function(){${await sandboxedResult.outputs[0]!.text()}})();`;
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
// Run "
|
|
48
|
+
const outputContent = `// Auto-generated file. Do not edit directly.
|
|
49
|
+
// Run "dash build.ts" from the package folder to regenerate.
|
|
50
50
|
|
|
51
51
|
// Full preload for trusted webviews (RPC, encryption, drag regions, webview tags)
|
|
52
52
|
export const preloadScript = ${JSON.stringify(fullPreloadJs)};
|
|
@@ -19,10 +19,16 @@ function uint8ArrayToBase64(uint8Array: Uint8Array): string {
|
|
|
19
19
|
return btoa(binary);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
function toArrayBuffer(bytes: Uint8Array): ArrayBuffer {
|
|
23
|
+
const buffer = new ArrayBuffer(bytes.byteLength);
|
|
24
|
+
new Uint8Array(buffer).set(bytes);
|
|
25
|
+
return buffer;
|
|
26
|
+
}
|
|
27
|
+
|
|
22
28
|
async function generateKeyFromBytes(rawKey: Uint8Array): Promise<CryptoKey> {
|
|
23
29
|
return await window.crypto.subtle.importKey(
|
|
24
30
|
"raw",
|
|
25
|
-
rawKey
|
|
31
|
+
toArrayBuffer(rawKey),
|
|
26
32
|
{ name: "AES-GCM" },
|
|
27
33
|
true,
|
|
28
34
|
["encrypt", "decrypt"],
|
|
@@ -41,9 +47,9 @@ export async function initEncryption(): Promise<void> {
|
|
|
41
47
|
const encodedText = encoder.encode(plaintext);
|
|
42
48
|
const iv = window.crypto.getRandomValues(new Uint8Array(12));
|
|
43
49
|
const encryptedBuffer = await window.crypto.subtle.encrypt(
|
|
44
|
-
{ name: "AES-GCM", iv },
|
|
50
|
+
{ name: "AES-GCM", iv: toArrayBuffer(iv) },
|
|
45
51
|
secretKey,
|
|
46
|
-
encodedText,
|
|
52
|
+
toArrayBuffer(encodedText),
|
|
47
53
|
);
|
|
48
54
|
|
|
49
55
|
// Split the tag (last 16 bytes) from the ciphertext
|
|
@@ -72,9 +78,9 @@ export async function initEncryption(): Promise<void> {
|
|
|
72
78
|
combinedData.set(tag, encryptedData.length);
|
|
73
79
|
|
|
74
80
|
const decryptedBuffer = await window.crypto.subtle.decrypt(
|
|
75
|
-
{ name: "AES-GCM", iv: iv
|
|
81
|
+
{ name: "AES-GCM", iv: toArrayBuffer(iv) },
|
|
76
82
|
secretKey,
|
|
77
|
-
combinedData
|
|
83
|
+
toArrayBuffer(combinedData),
|
|
78
84
|
);
|
|
79
85
|
|
|
80
86
|
const decoder = new TextDecoder();
|
|
@@ -6,6 +6,8 @@ declare global {
|
|
|
6
6
|
__electrobunWebviewId: number;
|
|
7
7
|
__electrobunWindowId: number;
|
|
8
8
|
__electrobunRpcSocketPort: number;
|
|
9
|
+
__electrobunHostSocketPort?: number;
|
|
10
|
+
__electrobunPlaintextHostSocket?: boolean;
|
|
9
11
|
__electrobunSecretKeyBytes: number[];
|
|
10
12
|
// Event-only bridge (all webviews, including sandboxed)
|
|
11
13
|
__electrobunEventBridge?: {
|
|
@@ -16,6 +18,9 @@ declare global {
|
|
|
16
18
|
postMessage: (message: string) => void;
|
|
17
19
|
};
|
|
18
20
|
// User RPC bridge (trusted webviews only)
|
|
21
|
+
__electrobunHostBridge?: {
|
|
22
|
+
postMessage: (message: string) => void;
|
|
23
|
+
};
|
|
19
24
|
__electrobunBunBridge?: {
|
|
20
25
|
postMessage: (message: string) => void;
|
|
21
26
|
};
|
|
@@ -28,7 +33,10 @@ declare global {
|
|
|
28
33
|
tag: string,
|
|
29
34
|
) => Promise<string>;
|
|
30
35
|
__electrobunSendToHost: (message: unknown) => void;
|
|
36
|
+
__electrobunPendingHostMessages?: unknown[];
|
|
31
37
|
__electrobun: {
|
|
38
|
+
receiveMessageFromHost: (msg: unknown) => void;
|
|
39
|
+
receiveInternalMessageFromHost: (msg: unknown) => void;
|
|
32
40
|
receiveMessageFromBun: (msg: unknown) => void;
|
|
33
41
|
receiveInternalMessageFromBun: (msg: unknown) => void;
|
|
34
42
|
};
|
|
@@ -7,10 +7,12 @@
|
|
|
7
7
|
// - window.__electrobunWebviewId
|
|
8
8
|
// - window.__electrobunWindowId
|
|
9
9
|
// - window.__electrobunRpcSocketPort
|
|
10
|
+
// - window.__electrobunHostSocketPort (optional alias)
|
|
10
11
|
// - window.__electrobunSecretKeyBytes
|
|
11
12
|
// - window.__electrobunEventBridge (event emission - all webviews)
|
|
12
13
|
// - window.__electrobunInternalBridge (internal RPC - trusted only)
|
|
13
|
-
// - window.
|
|
14
|
+
// - window.__electrobunHostBridge (user RPC - trusted only)
|
|
15
|
+
// - window.__electrobunBunBridge (legacy alias)
|
|
14
16
|
|
|
15
17
|
import "./globals.d.ts";
|
|
16
18
|
import { initEncryption } from "./encryption";
|
|
@@ -37,19 +39,27 @@ const internalMessageHandler = (msg: unknown) => {
|
|
|
37
39
|
handleResponse(msg as { type: string; id: string; success: boolean; payload: unknown });
|
|
38
40
|
};
|
|
39
41
|
|
|
42
|
+
const defaultUserMessageHandler = (msg: unknown) => {
|
|
43
|
+
// Buffer user RPC packets that arrive before the page-specific Electroview
|
|
44
|
+
// instance installs the real handler.
|
|
45
|
+
if (!window.__electrobunPendingHostMessages) {
|
|
46
|
+
window.__electrobunPendingHostMessages = [];
|
|
47
|
+
}
|
|
48
|
+
window.__electrobunPendingHostMessages.push(msg);
|
|
49
|
+
};
|
|
50
|
+
|
|
40
51
|
if (!window.__electrobun) {
|
|
41
52
|
window.__electrobun = {
|
|
53
|
+
receiveInternalMessageFromHost: internalMessageHandler,
|
|
54
|
+
receiveMessageFromHost: defaultUserMessageHandler,
|
|
42
55
|
receiveInternalMessageFromBun: internalMessageHandler,
|
|
43
|
-
receiveMessageFromBun:
|
|
44
|
-
// Default handler for user RPC - will be overridden if user creates Electroview
|
|
45
|
-
console.log("receiveMessageFromBun (no handler):", msg);
|
|
46
|
-
},
|
|
56
|
+
receiveMessageFromBun: defaultUserMessageHandler,
|
|
47
57
|
};
|
|
48
58
|
} else {
|
|
59
|
+
window.__electrobun.receiveInternalMessageFromHost = internalMessageHandler;
|
|
60
|
+
window.__electrobun.receiveMessageFromHost = defaultUserMessageHandler;
|
|
49
61
|
window.__electrobun.receiveInternalMessageFromBun = internalMessageHandler;
|
|
50
|
-
window.__electrobun.receiveMessageFromBun =
|
|
51
|
-
console.log("receiveMessageFromBun (no handler):", msg);
|
|
52
|
-
};
|
|
62
|
+
window.__electrobun.receiveMessageFromBun = defaultUserMessageHandler;
|
|
53
63
|
}
|
|
54
64
|
|
|
55
65
|
// Allow preload scripts to send custom messages to the host webview
|
|
@@ -68,6 +68,29 @@ export class ElectrobunWebviewTag extends HTMLElement {
|
|
|
68
68
|
if (this._sync) this._sync.stop();
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
private getInitialNavigationRules(): string[] | null {
|
|
72
|
+
const rawRules = this.getAttribute("navigation-rules");
|
|
73
|
+
if (rawRules === null) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const trimmed = rawRules.trim();
|
|
78
|
+
if (!trimmed) {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const parsed = JSON.parse(trimmed);
|
|
84
|
+
if (!Array.isArray(parsed) || !parsed.every((rule) => typeof rule === "string")) {
|
|
85
|
+
throw new Error("navigation-rules must be a JSON string array");
|
|
86
|
+
}
|
|
87
|
+
return parsed;
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error("Invalid navigation-rules attribute:", error);
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
71
94
|
async initWebview() {
|
|
72
95
|
const rect = this.getBoundingClientRect();
|
|
73
96
|
const initialRect = {
|
|
@@ -85,6 +108,7 @@ export class ElectrobunWebviewTag extends HTMLElement {
|
|
|
85
108
|
| "native"
|
|
86
109
|
| "cef";
|
|
87
110
|
const masks = this.getAttribute("masks");
|
|
111
|
+
const navigationRules = this.getInitialNavigationRules();
|
|
88
112
|
// Sandbox attribute: when present, the child webview is sandboxed (no RPC, events only)
|
|
89
113
|
const sandbox = this.hasAttribute("sandbox");
|
|
90
114
|
this.sandboxed = sandbox;
|
|
@@ -101,7 +125,7 @@ export class ElectrobunWebviewTag extends HTMLElement {
|
|
|
101
125
|
}
|
|
102
126
|
|
|
103
127
|
try {
|
|
104
|
-
const
|
|
128
|
+
const webviewInitParams = {
|
|
105
129
|
hostWebviewId: window.__electrobunWebviewId,
|
|
106
130
|
windowId: window.__electrobunWindowId,
|
|
107
131
|
renderer,
|
|
@@ -115,11 +139,16 @@ export class ElectrobunWebviewTag extends HTMLElement {
|
|
|
115
139
|
x: rect.x,
|
|
116
140
|
y: rect.y,
|
|
117
141
|
},
|
|
118
|
-
navigationRules: null,
|
|
119
142
|
sandbox,
|
|
120
143
|
transparent,
|
|
121
144
|
passthrough,
|
|
122
|
-
|
|
145
|
+
...(navigationRules === null ? {} : { navigationRules }),
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const webviewId = (await request(
|
|
149
|
+
"webviewTagInit",
|
|
150
|
+
webviewInitParams,
|
|
151
|
+
)) as number;
|
|
123
152
|
|
|
124
153
|
this.webviewId = webviewId;
|
|
125
154
|
this.id = `electrobun-webview-${webviewId}`;
|
|
@@ -17,6 +17,8 @@ export class ElectrobunWgpuTag extends HTMLElement {
|
|
|
17
17
|
transparent = false;
|
|
18
18
|
passthroughEnabled = false;
|
|
19
19
|
hidden = false;
|
|
20
|
+
private _ready = false;
|
|
21
|
+
private _initializing = false;
|
|
20
22
|
private _eventListeners: Record<string, Array<(event: CustomEvent) => void>> =
|
|
21
23
|
{};
|
|
22
24
|
|
|
@@ -25,18 +27,26 @@ export class ElectrobunWgpuTag extends HTMLElement {
|
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
connectedCallback() {
|
|
28
|
-
requestAnimationFrame(() =>
|
|
30
|
+
requestAnimationFrame(() => {
|
|
31
|
+
if (this.isConnected) void this.initWgpuView();
|
|
32
|
+
});
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
disconnectedCallback() {
|
|
32
36
|
if (this.wgpuViewId !== null) {
|
|
33
37
|
send("wgpuTagRemove", { id: this.wgpuViewId });
|
|
34
38
|
delete wgpuTagRegistry[this.wgpuViewId];
|
|
39
|
+
this.wgpuViewId = null;
|
|
35
40
|
}
|
|
36
41
|
if (this._sync) this._sync.stop();
|
|
42
|
+
this._sync = null;
|
|
43
|
+
this._ready = false;
|
|
37
44
|
}
|
|
38
45
|
|
|
39
46
|
async initWgpuView() {
|
|
47
|
+
if (this._initializing || this.wgpuViewId !== null) return;
|
|
48
|
+
this._initializing = true;
|
|
49
|
+
|
|
40
50
|
const rect = this.getBoundingClientRect();
|
|
41
51
|
const initialRect = {
|
|
42
52
|
x: rect.x,
|
|
@@ -74,8 +84,15 @@ export class ElectrobunWgpuTag extends HTMLElement {
|
|
|
74
84
|
passthrough,
|
|
75
85
|
})) as number;
|
|
76
86
|
|
|
87
|
+
if (!this.isConnected) {
|
|
88
|
+
send("wgpuTagRemove", { id: wgpuViewId });
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
77
92
|
this.wgpuViewId = wgpuViewId;
|
|
78
|
-
this.id
|
|
93
|
+
if (!this.id) {
|
|
94
|
+
this.id = `electrobun-wgpu-${wgpuViewId}`;
|
|
95
|
+
}
|
|
79
96
|
wgpuTagRegistry[wgpuViewId] = this;
|
|
80
97
|
|
|
81
98
|
this.setupObservers(initialRect);
|
|
@@ -96,9 +113,12 @@ export class ElectrobunWgpuTag extends HTMLElement {
|
|
|
96
113
|
});
|
|
97
114
|
});
|
|
98
115
|
|
|
116
|
+
this._ready = true;
|
|
99
117
|
this.emit("ready", { id: wgpuViewId });
|
|
100
118
|
} catch (err) {
|
|
101
119
|
console.error("Failed to init WGPU view:", err);
|
|
120
|
+
} finally {
|
|
121
|
+
this._initializing = false;
|
|
102
122
|
}
|
|
103
123
|
}
|
|
104
124
|
|
|
@@ -198,6 +218,17 @@ export class ElectrobunWgpuTag extends HTMLElement {
|
|
|
198
218
|
on(event: WgpuTagEventType, listener: (event: CustomEvent) => void) {
|
|
199
219
|
if (!this._eventListeners[event]) this._eventListeners[event] = [];
|
|
200
220
|
this._eventListeners[event].push(listener);
|
|
221
|
+
|
|
222
|
+
if (event === "ready" && this._ready && this.wgpuViewId !== null) {
|
|
223
|
+
const readyEvent = new CustomEvent(event, {
|
|
224
|
+
detail: { id: this.wgpuViewId },
|
|
225
|
+
});
|
|
226
|
+
queueMicrotask(() => {
|
|
227
|
+
if (this._eventListeners[event]?.includes(listener)) {
|
|
228
|
+
listener(readyEvent);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
}
|
|
201
232
|
}
|
|
202
233
|
|
|
203
234
|
off(event: WgpuTagEventType, listener: (event: CustomEvent) => void) {
|
|
@@ -1,14 +1,11 @@
|
|
|
1
|
-
// Contract tests for the bun:ffi APIs
|
|
1
|
+
// Contract tests for the bun:ffi compatibility APIs Electrobun depends on.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// breaks any of them, we want to know before cutting an electrobun release,
|
|
7
|
-
// not after a user reports a crash.
|
|
3
|
+
// Runtime changes can break JSCallback marshaling, FFIType encoding, or dlopen
|
|
4
|
+
// behavior. Keep these tests with Electrobun so Cottontail changes cannot
|
|
5
|
+
// silently break the native bridge.
|
|
8
6
|
//
|
|
9
|
-
// Skipped on Windows
|
|
10
|
-
// the
|
|
11
|
-
// the libc path resolution to include msvcrt/ucrtbase.
|
|
7
|
+
// Skipped on Windows because the system library paths differ. To enable them,
|
|
8
|
+
// extend the libc path resolution to include msvcrt/ucrtbase.
|
|
12
9
|
|
|
13
10
|
import { describe, expect, it } from "bun:test";
|
|
14
11
|
import {
|