From f6978f44b211b77b800b0bd939a8be8599f9078d Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Sun, 2 Aug 2026 03:19:36 +0100 Subject: [PATCH] Enhance Electrobun integration and pre-build process - Updated pre-build script to create a stub for the dev environment, preventing warnings during Vite development. - Refactored electrobun-bridge.js to improve initialization logic and ensure proper setup of the Electrobun RPC. - Modified ElectronContext to dynamically retrieve the desktop API, enhancing compatibility and maintainability. - Integrated Electrobun bridge initialization in the main application entry point for improved startup behavior. --- scripts/pre-build.mjs | 12 ++- .../Dashboard/context/ElectronContext.jsx | 53 ++++++---- src/electrobun-bridge.js | 98 ++++++++++++++++--- src/index.jsx | 4 +- 4 files changed, 131 insertions(+), 36 deletions(-) diff --git a/scripts/pre-build.mjs b/scripts/pre-build.mjs index e6717a7..32046ce 100644 --- a/scripts/pre-build.mjs +++ b/scripts/pre-build.mjs @@ -1,4 +1,4 @@ -import { existsSync } from "node:fs"; +import { existsSync, mkdirSync, writeFileSync } from "node:fs"; import path from "node:path"; import { spawnSync } from "node:child_process"; import { fileURLToPath } from "node:url"; @@ -81,6 +81,16 @@ if (buildEnv === "dev") { console.log( "pre-build: dev environment — skipping production renderer build (use dev:renderer for Vite)", ); + + // Electrobun still copies dist/mainview into the app bundle; provide a stub so + // dev builds don't warn when Vite serves the renderer instead. + const stubDir = path.join(rootDir, "dist/mainview"); + mkdirSync(stubDir, { recursive: true }); + writeFileSync( + path.join(stubDir, "index.html"), + "Dev mode — start Vite with bun run dev:renderer.", + ); + console.log(`pre-build: validation passed (${buildEnv})`); process.exit(0); } diff --git a/src/components/Dashboard/context/ElectronContext.jsx b/src/components/Dashboard/context/ElectronContext.jsx index a2886c2..9f54d89 100644 --- a/src/components/Dashboard/context/ElectronContext.jsx +++ b/src/components/Dashboard/context/ElectronContext.jsx @@ -4,10 +4,15 @@ import { useNavigate } from 'react-router-dom' const electron = window.require ? window.require('electron') : null const ipcRenderer = electron ? electron.ipcRenderer : null -const desktopAPI = window.electronAPI + +function getDesktopAPI() { + return window.electronAPI +} // eslint-disable-next-line react-refresh/only-export-components export function isElectron() { + const desktopAPI = getDesktopAPI() + if (desktopAPI?.isDesktop) { return true } @@ -61,7 +66,12 @@ const ElectronProvider = ({ children }) => { const [isMaximized, setIsMaximized] = useState(false) const [isFullScreen, setIsFullScreen] = useState(false) const [electronAvailable] = useState(isElectron()) - const useElectrobun = Boolean(desktopAPI?.isDesktop) + const useElectrobun = Boolean( + getDesktopAPI()?.isDesktop || + (typeof window !== 'undefined' && + window.__electrobunWebviewId && + window.__electrobunRpcSocketPort) + ) const navigate = useNavigate() const lastNavigationAtRef = useRef(0) @@ -91,7 +101,7 @@ const ElectronProvider = ({ children }) => { const openExternalUrl = (url) => { if (useElectrobun) { - desktopAPI.openExternalUrl(url) + getDesktopAPI().openExternalUrl(url) return true } if (electronAvailable && ipcRenderer) { @@ -103,7 +113,7 @@ const ElectronProvider = ({ children }) => { const openInternalUrl = (url) => { if (useElectrobun) { - desktopAPI.openInternalUrl(url) + getDesktopAPI().openInternalUrl(url) return true } if (electronAvailable && ipcRenderer) { @@ -117,7 +127,7 @@ const ElectronProvider = ({ children }) => { if (!electronAvailable) return if (useElectrobun) { - desktopAPI.getOsInfo().then((info) => { + getDesktopAPI().getOsInfo().then((info) => { if (info?.platform) { setPlatform(info.platform) if (info.platform === 'darwin') { @@ -126,13 +136,16 @@ const ElectronProvider = ({ children }) => { } }) - desktopAPI.getWindowState().then(applyWindowState) + getDesktopAPI().getWindowState().then(applyWindowState) - const unsubWindowState = desktopAPI.onMessage('windowState', applyWindowState) - const unsubNavigate = desktopAPI.onMessage('navigate', (url) => { + const unsubWindowState = getDesktopAPI().onMessage( + 'windowState', + applyWindowState + ) + const unsubNavigate = getDesktopAPI().onMessage('navigate', (url) => { navigate(url) }) - const unsubNavigationGesture = desktopAPI.onMessage( + const unsubNavigationGesture = getDesktopAPI().onMessage( 'navigationGesture', navigateHistory ) @@ -217,7 +230,7 @@ const ElectronProvider = ({ children }) => { const handleWindowControl = (action) => { if (useElectrobun) { - desktopAPI.windowControl(action) + getDesktopAPI().windowControl(action) return } if (electronAvailable && ipcRenderer) { @@ -227,7 +240,7 @@ const ElectronProvider = ({ children }) => { const getAuthSession = async () => { if (!electronAvailable) return null - if (useElectrobun) return await desktopAPI.getAuthSession() + if (useElectrobun) return await getDesktopAPI().getAuthSession() if (!ipcRenderer) return null return await ipcRenderer.invoke('auth-session-get') } @@ -235,7 +248,7 @@ const ElectronProvider = ({ children }) => { const setAuthSession = async (session) => { if (!electronAvailable) return false if (useElectrobun) { - const result = await desktopAPI.setAuthSession(session) + const result = await getDesktopAPI().setAuthSession(session) return result?.ok ?? false } if (!ipcRenderer) return false @@ -245,7 +258,7 @@ const ElectronProvider = ({ children }) => { const clearAuthSession = async () => { if (!electronAvailable) return false if (useElectrobun) { - const result = await desktopAPI.clearAuthSession() + const result = await getDesktopAPI().clearAuthSession() return result?.ok ?? false } if (!ipcRenderer) return false @@ -254,7 +267,7 @@ const ElectronProvider = ({ children }) => { const getAppSettings = useCallback(async () => { if (!electronAvailable) return {} - if (useElectrobun) return await desktopAPI.getAppSettings() + if (useElectrobun) return await getDesktopAPI().getAppSettings() if (!ipcRenderer) return {} return await ipcRenderer.invoke('app-settings-get') }, [electronAvailable, useElectrobun]) @@ -263,7 +276,7 @@ const ElectronProvider = ({ children }) => { async (settings) => { if (!electronAvailable) return false if (useElectrobun) { - const result = await desktopAPI.setAppSettings(settings) + const result = await getDesktopAPI().setAppSettings(settings) return result?.ok ?? false } if (!ipcRenderer) return false @@ -276,7 +289,7 @@ const ElectronProvider = ({ children }) => { async (update) => { if (!electronAvailable) return false if (useElectrobun) { - const result = await desktopAPI.startAppUpdate(update) + const result = await getDesktopAPI().startAppUpdate(update) return result?.ok ?? false } if (!ipcRenderer) return false @@ -292,7 +305,7 @@ const ElectronProvider = ({ children }) => { } if (useElectrobun) { - return desktopAPI.onMessage('appUpdateProgress', handler) + return getDesktopAPI().onMessage('appUpdateProgress', handler) } if (!ipcRenderer) return () => {} @@ -324,7 +337,7 @@ const ElectronProvider = ({ children }) => { if (!electronAvailable) return false try { if (useElectrobun) { - const result = await desktopAPI.resizeSpotlightWindow(height) + const result = await getDesktopAPI().resizeSpotlightWindow(height) return result?.ok ?? false } if (!ipcRenderer) return false @@ -342,7 +355,7 @@ const ElectronProvider = ({ children }) => { async (sections) => { if (!electronAvailable) return false if (useElectrobun) { - const result = await desktopAPI.setSidebarViewMenu(sections) + const result = await getDesktopAPI().setSidebarViewMenu(sections) return result?.ok ?? false } if (!ipcRenderer) return false @@ -353,7 +366,7 @@ const ElectronProvider = ({ children }) => { const getElectronVersion = useCallback(async () => { if (!electronAvailable) return null - if (useElectrobun) return await desktopAPI.getAppVersion() + if (useElectrobun) return await getDesktopAPI().getAppVersion() if (!ipcRenderer) return null return await ipcRenderer.invoke('electron-version') }, [electronAvailable, useElectrobun]) diff --git a/src/electrobun-bridge.js b/src/electrobun-bridge.js index fe2a254..d2cb199 100644 --- a/src/electrobun-bridge.js +++ b/src/electrobun-bridge.js @@ -1,14 +1,16 @@ -import Electrobun, { Electroview } from "electrobun/view"; - -const isDesktop = Boolean( - typeof window !== "undefined" && - window.__electrobunWebviewId && - window.__electrobunRpcSocketPort, -); - const listeners = new Map(); const pendingMessages = new Map(); let rpc = null; +let initPromise = null; +let initialized = false; + +export function isElectrobunDesktop() { + return Boolean( + typeof window !== "undefined" && + window.__electrobunWebviewId && + window.__electrobunRpcSocketPort, + ); +} function dispatchMessage(channel, data) { const channelListeners = listeners.get(channel); @@ -25,7 +27,11 @@ function dispatchMessage(channel, data) { } } -if (isDesktop) { +async function setupRpc() { + // electrobun/view captures webview globals at import time — import only after + // Electrobun preload has set them (Vite dev can evaluate modules earlier). + const { default: Electrobun, Electroview } = await import("electrobun/view"); + rpc = Electroview.defineRPC({ maxRequestTime: 30000, handlers: { @@ -41,6 +47,70 @@ if (isDesktop) { new Electrobun.Electroview({ rpc }); } +function shouldWaitForElectrobun() { + if (isElectrobunDesktop()) { + return true; + } + + if (typeof window === "undefined") { + return false; + } + + if (window.__electrobunWebviewId) { + return true; + } + + const { protocol, hostname, port } = window.location; + + if (protocol === "views:") { + return true; + } + + // Vite dev server used by electrobun dev:app + if (hostname === "localhost" && port === "5780") { + return true; + } + + return Boolean( + window.__electrobun || + window.__electrobunEventBridge || + window.__electrobunInternalBridge, + ); +} + +export async function initElectrobunBridge() { + if (initialized) { + return; + } + + if (!initPromise) { + initPromise = (async () => { + if (!shouldWaitForElectrobun()) { + return; + } + + const deadline = Date.now() + 3000; + + while (Date.now() < deadline) { + if (isElectrobunDesktop()) { + await setupRpc(); + window.electronAPI = electronAPI; + initialized = true; + return; + } + + await new Promise((resolve) => setTimeout(resolve, 50)); + } + + console.warn( + "Electrobun bridge: webview globals not found; desktop RPC unavailable.", + ); + })(); + } + + await initPromise; +} + function onMessage(channel, callback) { if (!listeners.has(channel)) { listeners.set(channel, new Set()); @@ -66,6 +136,8 @@ function removeAllListeners(channel) { } async function invokeRequest(method, params = {}) { + await initElectrobunBridge(); + if (!rpc?.request?.[method]) { console.warn(`Unhandled RPC request: ${method}`); return null; @@ -75,7 +147,9 @@ async function invokeRequest(method, params = {}) { } const electronAPI = { - isDesktop, + get isDesktop() { + return isElectrobunDesktop(); + }, onMessage, removeAllListeners, getOsInfo: () => invokeRequest("getOsInfo"), @@ -96,8 +170,4 @@ const electronAPI = { getAppVersion: () => invokeRequest("getAppVersion"), }; -if (isDesktop) { - window.electronAPI = electronAPI; -} - export default electronAPI; diff --git a/src/index.jsx b/src/index.jsx index 6d93b52..c901182 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -1,9 +1,11 @@ -import './electrobun-bridge.js' +import { initElectrobunBridge } from './electrobun-bridge.js' import reportWebVitals from './reportWebVitals' import ReactDOM from 'react-dom/client' import FarmControlApp from './App' import React from 'react' +await initElectrobunBridge() + const root = ReactDOM.createRoot(document.getElementById('root')) root.render(