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 path from "node:path";
import { spawnSync } from "node:child_process"; import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
@ -81,6 +81,16 @@ if (buildEnv === "dev") {
console.log( console.log(
"pre-build: dev environment — skipping production renderer build (use dev:renderer for Vite)", "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})`); console.log(`pre-build: validation passed (${buildEnv})`);
process.exit(0); process.exit(0);
} }

View File

@ -4,10 +4,15 @@ import { useNavigate } from 'react-router-dom'
const electron = window.require ? window.require('electron') : null const electron = window.require ? window.require('electron') : null
const ipcRenderer = electron ? electron.ipcRenderer : 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 // eslint-disable-next-line react-refresh/only-export-components
export function isElectron() { export function isElectron() {
const desktopAPI = getDesktopAPI()
if (desktopAPI?.isDesktop) { if (desktopAPI?.isDesktop) {
return true return true
} }
@ -61,7 +66,12 @@ const ElectronProvider = ({ children }) => {
const [isMaximized, setIsMaximized] = useState(false) const [isMaximized, setIsMaximized] = useState(false)
const [isFullScreen, setIsFullScreen] = useState(false) const [isFullScreen, setIsFullScreen] = useState(false)
const [electronAvailable] = useState(isElectron()) 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 navigate = useNavigate()
const lastNavigationAtRef = useRef(0) const lastNavigationAtRef = useRef(0)
@ -91,7 +101,7 @@ const ElectronProvider = ({ children }) => {
const openExternalUrl = (url) => { const openExternalUrl = (url) => {
if (useElectrobun) { if (useElectrobun) {
desktopAPI.openExternalUrl(url) getDesktopAPI().openExternalUrl(url)
return true return true
} }
if (electronAvailable && ipcRenderer) { if (electronAvailable && ipcRenderer) {
@ -103,7 +113,7 @@ const ElectronProvider = ({ children }) => {
const openInternalUrl = (url) => { const openInternalUrl = (url) => {
if (useElectrobun) { if (useElectrobun) {
desktopAPI.openInternalUrl(url) getDesktopAPI().openInternalUrl(url)
return true return true
} }
if (electronAvailable && ipcRenderer) { if (electronAvailable && ipcRenderer) {
@ -117,7 +127,7 @@ const ElectronProvider = ({ children }) => {
if (!electronAvailable) return if (!electronAvailable) return
if (useElectrobun) { if (useElectrobun) {
desktopAPI.getOsInfo().then((info) => { getDesktopAPI().getOsInfo().then((info) => {
if (info?.platform) { if (info?.platform) {
setPlatform(info.platform) setPlatform(info.platform)
if (info.platform === 'darwin') { 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 unsubWindowState = getDesktopAPI().onMessage(
const unsubNavigate = desktopAPI.onMessage('navigate', (url) => { 'windowState',
applyWindowState
)
const unsubNavigate = getDesktopAPI().onMessage('navigate', (url) => {
navigate(url) navigate(url)
}) })
const unsubNavigationGesture = desktopAPI.onMessage( const unsubNavigationGesture = getDesktopAPI().onMessage(
'navigationGesture', 'navigationGesture',
navigateHistory navigateHistory
) )
@ -217,7 +230,7 @@ const ElectronProvider = ({ children }) => {
const handleWindowControl = (action) => { const handleWindowControl = (action) => {
if (useElectrobun) { if (useElectrobun) {
desktopAPI.windowControl(action) getDesktopAPI().windowControl(action)
return return
} }
if (electronAvailable && ipcRenderer) { if (electronAvailable && ipcRenderer) {
@ -227,7 +240,7 @@ const ElectronProvider = ({ children }) => {
const getAuthSession = async () => { const getAuthSession = async () => {
if (!electronAvailable) return null if (!electronAvailable) return null
if (useElectrobun) return await desktopAPI.getAuthSession() if (useElectrobun) return await getDesktopAPI().getAuthSession()
if (!ipcRenderer) return null if (!ipcRenderer) return null
return await ipcRenderer.invoke('auth-session-get') return await ipcRenderer.invoke('auth-session-get')
} }
@ -235,7 +248,7 @@ const ElectronProvider = ({ children }) => {
const setAuthSession = async (session) => { const setAuthSession = async (session) => {
if (!electronAvailable) return false if (!electronAvailable) return false
if (useElectrobun) { if (useElectrobun) {
const result = await desktopAPI.setAuthSession(session) const result = await getDesktopAPI().setAuthSession(session)
return result?.ok ?? false return result?.ok ?? false
} }
if (!ipcRenderer) return false if (!ipcRenderer) return false
@ -245,7 +258,7 @@ const ElectronProvider = ({ children }) => {
const clearAuthSession = async () => { const clearAuthSession = async () => {
if (!electronAvailable) return false if (!electronAvailable) return false
if (useElectrobun) { if (useElectrobun) {
const result = await desktopAPI.clearAuthSession() const result = await getDesktopAPI().clearAuthSession()
return result?.ok ?? false return result?.ok ?? false
} }
if (!ipcRenderer) return false if (!ipcRenderer) return false
@ -254,7 +267,7 @@ const ElectronProvider = ({ children }) => {
const getAppSettings = useCallback(async () => { const getAppSettings = useCallback(async () => {
if (!electronAvailable) return {} if (!electronAvailable) return {}
if (useElectrobun) return await desktopAPI.getAppSettings() if (useElectrobun) return await getDesktopAPI().getAppSettings()
if (!ipcRenderer) return {} if (!ipcRenderer) return {}
return await ipcRenderer.invoke('app-settings-get') return await ipcRenderer.invoke('app-settings-get')
}, [electronAvailable, useElectrobun]) }, [electronAvailable, useElectrobun])
@ -263,7 +276,7 @@ const ElectronProvider = ({ children }) => {
async (settings) => { async (settings) => {
if (!electronAvailable) return false if (!electronAvailable) return false
if (useElectrobun) { if (useElectrobun) {
const result = await desktopAPI.setAppSettings(settings) const result = await getDesktopAPI().setAppSettings(settings)
return result?.ok ?? false return result?.ok ?? false
} }
if (!ipcRenderer) return false if (!ipcRenderer) return false
@ -276,7 +289,7 @@ const ElectronProvider = ({ children }) => {
async (update) => { async (update) => {
if (!electronAvailable) return false if (!electronAvailable) return false
if (useElectrobun) { if (useElectrobun) {
const result = await desktopAPI.startAppUpdate(update) const result = await getDesktopAPI().startAppUpdate(update)
return result?.ok ?? false return result?.ok ?? false
} }
if (!ipcRenderer) return false if (!ipcRenderer) return false
@ -292,7 +305,7 @@ const ElectronProvider = ({ children }) => {
} }
if (useElectrobun) { if (useElectrobun) {
return desktopAPI.onMessage('appUpdateProgress', handler) return getDesktopAPI().onMessage('appUpdateProgress', handler)
} }
if (!ipcRenderer) return () => {} if (!ipcRenderer) return () => {}
@ -324,7 +337,7 @@ const ElectronProvider = ({ children }) => {
if (!electronAvailable) return false if (!electronAvailable) return false
try { try {
if (useElectrobun) { if (useElectrobun) {
const result = await desktopAPI.resizeSpotlightWindow(height) const result = await getDesktopAPI().resizeSpotlightWindow(height)
return result?.ok ?? false return result?.ok ?? false
} }
if (!ipcRenderer) return false if (!ipcRenderer) return false
@ -342,7 +355,7 @@ const ElectronProvider = ({ children }) => {
async (sections) => { async (sections) => {
if (!electronAvailable) return false if (!electronAvailable) return false
if (useElectrobun) { if (useElectrobun) {
const result = await desktopAPI.setSidebarViewMenu(sections) const result = await getDesktopAPI().setSidebarViewMenu(sections)
return result?.ok ?? false return result?.ok ?? false
} }
if (!ipcRenderer) return false if (!ipcRenderer) return false
@ -353,7 +366,7 @@ const ElectronProvider = ({ children }) => {
const getElectronVersion = useCallback(async () => { const getElectronVersion = useCallback(async () => {
if (!electronAvailable) return null if (!electronAvailable) return null
if (useElectrobun) return await desktopAPI.getAppVersion() if (useElectrobun) return await getDesktopAPI().getAppVersion()
if (!ipcRenderer) return null if (!ipcRenderer) return null
return await ipcRenderer.invoke('electron-version') return await ipcRenderer.invoke('electron-version')
}, [electronAvailable, useElectrobun]) }, [electronAvailable, useElectrobun])

View File

@ -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 listeners = new Map();
const pendingMessages = new Map(); const pendingMessages = new Map();
let rpc = null; 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) { function dispatchMessage(channel, data) {
const channelListeners = listeners.get(channel); 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({ rpc = Electroview.defineRPC({
maxRequestTime: 30000, maxRequestTime: 30000,
handlers: { handlers: {
@ -41,6 +47,70 @@ if (isDesktop) {
new Electrobun.Electroview({ rpc }); 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) { function onMessage(channel, callback) {
if (!listeners.has(channel)) { if (!listeners.has(channel)) {
listeners.set(channel, new Set()); listeners.set(channel, new Set());
@ -66,6 +136,8 @@ function removeAllListeners(channel) {
} }
async function invokeRequest(method, params = {}) { async function invokeRequest(method, params = {}) {
await initElectrobunBridge();
if (!rpc?.request?.[method]) { if (!rpc?.request?.[method]) {
console.warn(`Unhandled RPC request: ${method}`); console.warn(`Unhandled RPC request: ${method}`);
return null; return null;
@ -75,7 +147,9 @@ async function invokeRequest(method, params = {}) {
} }
const electronAPI = { const electronAPI = {
isDesktop, get isDesktop() {
return isElectrobunDesktop();
},
onMessage, onMessage,
removeAllListeners, removeAllListeners,
getOsInfo: () => invokeRequest("getOsInfo"), getOsInfo: () => invokeRequest("getOsInfo"),
@ -96,8 +170,4 @@ const electronAPI = {
getAppVersion: () => invokeRequest("getAppVersion"), getAppVersion: () => invokeRequest("getAppVersion"),
}; };
if (isDesktop) {
window.electronAPI = electronAPI;
}
export default 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 reportWebVitals from './reportWebVitals'
import ReactDOM from 'react-dom/client' import ReactDOM from 'react-dom/client'
import FarmControlApp from './App' import FarmControlApp from './App'
import React from 'react' import React from 'react'
await initElectrobunBridge()
const root = ReactDOM.createRoot(document.getElementById('root')) const root = ReactDOM.createRoot(document.getElementById('root'))
root.render( root.render(
<React.StrictMode> <React.StrictMode>