UNPKG

@wuchuheng/electron

Version:
188 lines (157 loc) 6.69 kB
import { BrowserWindow, dialog } from 'electron'; import net from 'node:net'; import { logger } from '../utils/logger'; declare const MAIN_WINDOW_WEBPACK_ENTRY: string; declare const MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY: string; const parseEntryPort = () => { try { const url = new URL(MAIN_WINDOW_WEBPACK_ENTRY); const port = Number(url.port) || (url.protocol === 'https:' ? 443 : 80); const isHttp = url.protocol === 'http:' || url.protocol === 'https:'; const isLocalhost = ['localhost', '127.0.0.1', '::1'].includes(url.hostname); return { host: url.hostname, port, isHttp, isLocalhost, description: `${url.hostname}:${port}` }; } catch { return null; } }; const isPortReachable = (host: string, port: number, timeoutMs = 750): Promise<boolean> => { return new Promise(resolve => { const socket = net.createConnection({ host, port }); const cleanup = () => { socket.removeAllListeners(); socket.end(); socket.destroy(); }; socket.setTimeout(timeoutMs); socket.once('connect', () => { cleanup(); resolve(true); }); const onError = () => { cleanup(); resolve(false); }; socket.once('error', onError); socket.once('timeout', onError); }); }; const ensureRendererAvailable = async (entryUrl: string, entryMeta: ReturnType<typeof parseEntryPort>) => { if (!entryMeta?.isHttp || !entryMeta.isLocalhost) return; const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 2000); try { const response = await fetch(entryUrl, { method: 'HEAD', signal: controller.signal }); if (response.ok) return; const reachable = await isPortReachable(entryMeta.host, entryMeta.port); const reason = reachable ? `Port ${entryMeta.port} is serving a different app (HTTP ${response.status}).` : `Renderer dev server is not running on ${entryMeta.description}.`; throw new Error(reason); } catch (error) { const reachable = entryMeta ? await isPortReachable(entryMeta.host, entryMeta.port) : false; const details = error instanceof Error ? error.message : String(error); const reason = reachable ? `Port ${entryMeta?.port ?? 'unknown'} is already in use by another process.` : `Renderer dev server is not reachable at ${entryMeta?.description ?? entryUrl}.`; throw new Error(`${reason} (${details})`); } finally { clearTimeout(timeout); } }; export const createWindow = async (): Promise<BrowserWindow> => { logger.info('Creating main window'); const entryMeta = parseEntryPort(); if (process.env.NODE_ENV === 'development') { try { await ensureRendererAvailable(MAIN_WINDOW_WEBPACK_ENTRY, entryMeta); } catch (error) { const message = error instanceof Error ? error.message : String(error); logger.error(`Renderer entry not reachable: ${message}`); dialog.showErrorBox('Renderer failed to load', message); throw error; } } try { // Create the browser window. logger.info(`Creating BrowserWindow with preload path: ${MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY}`); logger.info(`Main window entry: ${MAIN_WINDOW_WEBPACK_ENTRY}`); const mainWindow = new BrowserWindow({ height: 800 * 1.5, width: 1200 * 1.5, webPreferences: { preload: MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY, contextIsolation: true, nodeIntegration: false, }, // Use frameless window for custom title bar frame: false, // No need for autoHideMenuBar with frameless window // autoHideMenuBar: true, titleBarStyle: 'hidden', // Add background color to prevent white flash during loading backgroundColor: '#1e1e2e', }); logger.info('BrowserWindow created successfully'); // Hide the menu bar completely mainWindow.setMenuBarVisibility(false); logger.info('Menu bar visibility set to false'); // Verify contentView is created logger.info(`Main window contentView exists: ${!!mainWindow.contentView}`); // Log some window properties for debugging logger.info(`Window bounds: ${JSON.stringify(mainWindow.getBounds())}`); logger.info(`Window is visible: ${mainWindow.isVisible()}`); logger.info(`Window is minimized: ${mainWindow.isMinimized()}`); logger.info(`Window is maximized: ${mainWindow.isMaximized()}`); logger.info(`Window is fullscreen: ${mainWindow.isFullScreen()}`); // and load the index.html of the app. mainWindow.loadURL(MAIN_WINDOW_WEBPACK_ENTRY); logger.info(`Loading URL: ${MAIN_WINDOW_WEBPACK_ENTRY}`); // Give a clearer hint when the renderer fails to load (often a dev-server port clash) if (entryMeta?.isHttp && entryMeta.isLocalhost) { mainWindow.webContents.on( 'did-fail-load', async (_event, errorCode, errorDescription, validatedURL, isMainFrame) => { if (!isMainFrame) return; const reachable = await isPortReachable(entryMeta.host, entryMeta.port); const base = `${errorCode}: ${errorDescription} while loading ${validatedURL || MAIN_WINDOW_WEBPACK_ENTRY}`; const hint = reachable ? `Port ${entryMeta.port} is already in use by another process. Stop that process or set WEBPACK_DEV_SERVER_PORT to a free port.` : `Renderer dev server is not reachable at ${entryMeta.description}. It may have failed to start.`; const message = `${base}.\n${hint}`; logger.error(message); dialog.showErrorBox('Renderer failed to load', message); } ); } // Open the DevTools in development mode only if (process.env.NODE_ENV === 'development') { logger.info('Opening DevTools in development mode'); mainWindow.webContents.openDevTools(); } // Log when the window is ready to show mainWindow.once('ready-to-show', () => { logger.info('Main window is ready to show'); }); // Log window lifecycle events mainWindow.on('show', () => { logger.info('Main window shown'); }); mainWindow.on('hide', () => { logger.info('Main window hidden'); }); mainWindow.on('focus', () => { logger.info('Main window focused'); }); mainWindow.on('blur', () => { logger.info('Main window blurred'); }); mainWindow.on('close', () => { logger.info('Main window closing'); }); return mainWindow; } catch (error) { logger.error(`Error creating main window: ${error instanceof Error ? error.message : String(error)}`); logger.error(`Error stack: ${error instanceof Error ? error.stack : 'No stack available'}`); throw error; } };