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 withbun 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(