@wuchuheng/electron
Version:
Electron project template
188 lines (157 loc) • 6.69 kB
text/typescript
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;
}
};