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.
This commit is contained in:
Tom Butcher 2026-08-02 03:19:36 +01:00
parent f59000778e
commit f6978f44b2
4 changed files with 131 additions and 36 deletions

View File

@ -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"),
"<!doctype html><html><body>Dev mode — start Vite with <code>bun run dev:renderer</code>.</body></html>",
);
console.log(`pre-build: validation passed (${buildEnv})`);
process.exit(0);
}

View File

@ -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])

View File

@ -1,14 +1,16 @@
import Electrobun, { Electroview } from "electrobun/view";
const listeners = new Map();
const pendingMessages = new Map();
let rpc = null;
let initPromise = null;
let initialized = false;
const isDesktop = Boolean(
export function isElectrobunDesktop() {
return Boolean(
typeof window !== "undefined" &&
window.__electrobunWebviewId &&
window.__electrobunRpcSocketPort,
);
const listeners = new Map();
const pendingMessages = new Map();
let rpc = null;
}
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;

View File

@ -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(
<React.StrictMode>