From df50b6b6807b792c5c7394f870958e4101098285 Mon Sep 17 00:00:00 2001 From: Ivan Fontosh Date: Mon, 14 Sep 2026 07:39:46 +0800 Subject: [PATCH] Fix black screen: wait for Vite before Electron, retry loadURL, show boot fallback. Co-authored-by: Cursor --- app/main/index.ts | 30 +++++++++++++++++++++++++++++- app/renderer/App.tsx | 18 ++++++++++++++---- app/renderer/index.html | 6 +++++- app/renderer/main.tsx | 19 ++++++++++++++++++- scripts/dev.mjs | 38 +++++++++++++++++++++++++++++++++++--- vite.config.ts | 1 + 6 files changed, 102 insertions(+), 10 deletions(-) diff --git a/app/main/index.ts b/app/main/index.ts index a010a9a..fdedcd9 100644 --- a/app/main/index.ts +++ b/app/main/index.ts @@ -69,9 +69,37 @@ function createWindow(): void { }, }); + mainWindow.once('ready-to-show', () => { + mainWindow?.show(); + mainWindow?.focus(); + }); + mainWindow.show(); + const devUrl = process.env.VITE_DEV_SERVER_URL; if (devUrl) { - void mainWindow.loadURL(devUrl); + mainWindow.webContents.openDevTools({ mode: 'detach' }); + mainWindow.webContents.on('did-fail-load', (_e, code, desc, url) => { + console.error('did-fail-load', code, desc, url); + }); + mainWindow.webContents.on('console-message', (_e, level, message) => { + console.log(`[renderer:${level}]`, message); + }); + const load = async (attempt = 0) => { + try { + await mainWindow!.loadURL(devUrl); + console.log('Loaded', devUrl); + } catch (err) { + console.error('loadURL attempt', attempt, err); + if (attempt < 20) { + setTimeout(() => void load(attempt + 1), 300); + return; + } + await mainWindow!.loadURL( + `data:text/html,

Dev server not reachable: ${devUrl}

`, + ); + } + }; + void load(); } else { void mainWindow.loadFile(path.join(__dirname, '../renderer/index.html')); } diff --git a/app/renderer/App.tsx b/app/renderer/App.tsx index c2b16f9..f4d22db 100644 --- a/app/renderer/App.tsx +++ b/app/renderer/App.tsx @@ -155,12 +155,22 @@ export function App() { void (async () => { if (!window.ttrpgBuilder) { setBooting(false); + setStatus( + locale === 'ru' + ? 'Нет связи с Electron preload (перезапустите npm run dev)' + : 'Electron preload missing (restart npm run dev)', + ); return; } - await window.ttrpgBuilder.seedPresets(); - await refresh(); - setBooting(false); - setStatus(locale === 'ru' ? 'Пресеты готовы' : 'Presets ready'); + try { + await window.ttrpgBuilder.seedPresets(); + await refresh(); + setStatus(locale === 'ru' ? 'Пресеты готовы' : 'Presets ready'); + } catch (e) { + setStatus(String(e)); + } finally { + setBooting(false); + } })(); }, [refresh, locale]); diff --git a/app/renderer/index.html b/app/renderer/index.html index 0cdd555..e63f308 100644 --- a/app/renderer/index.html +++ b/app/renderer/index.html @@ -4,9 +4,13 @@ TTRPG Character Builder + -
+
Loading TTRPG Character Builder…
diff --git a/app/renderer/main.tsx b/app/renderer/main.tsx index 5d922b8..de0f83e 100644 --- a/app/renderer/main.tsx +++ b/app/renderer/main.tsx @@ -2,4 +2,21 @@ import { createRoot } from 'react-dom/client'; import { App } from './App.tsx'; import './styles.css'; -createRoot(document.getElementById('root')!).render(); +const rootEl = document.getElementById('root'); +if (!rootEl) { + document.body.innerHTML = '
#root missing
'; +} else { + try { + createRoot(rootEl).render(); + } catch (err) { + rootEl.innerHTML = `
${String(err)}
`; + console.error(err); + } +} + +window.addEventListener('error', (e) => { + console.error('window.error', e.error || e.message); +}); +window.addEventListener('unhandledrejection', (e) => { + console.error('unhandledrejection', e.reason); +}); diff --git a/scripts/dev.mjs b/scripts/dev.mjs index da42f49..5cc58cd 100644 --- a/scripts/dev.mjs +++ b/scripts/dev.mjs @@ -3,9 +3,11 @@ import { spawn } from 'node:child_process'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import fs from 'node:fs'; +import http from 'node:http'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const root = path.resolve(__dirname, '..'); +const DEV_URL = 'http://127.0.0.1:5174/'; async function buildMain() { await build({ @@ -35,24 +37,54 @@ function electronPath() { return path.join(root, 'node_modules', 'electron', 'cli.js'); } +function waitForUrl(url, timeoutMs = 60000) { + const start = Date.now(); + return new Promise((resolve, reject) => { + const tick = () => { + const req = http.get(url, (res) => { + res.resume(); + resolve(true); + }); + req.on('error', () => { + if (Date.now() - start > timeoutMs) { + reject(new Error(`Timed out waiting for ${url}`)); + return; + } + setTimeout(tick, 250); + }); + }; + tick(); + }); +} + await buildMain(); const vite = spawn( process.platform === 'win32' ? 'npx.cmd' : 'npx', - ['vite', '--config', 'vite.config.ts'], + ['vite', '--config', 'vite.config.ts', '--host', '127.0.0.1', '--port', '5174'], { cwd: root, stdio: 'inherit', shell: true }, ); -await new Promise((r) => setTimeout(r, 2000)); +console.log('Waiting for Vite…'); +await waitForUrl(DEV_URL); +console.log('Vite is ready, launching Electron…'); const elec = spawn(electronPath(), ['.'], { cwd: root, stdio: 'inherit', shell: true, - env: { ...process.env, VITE_DEV_SERVER_URL: 'http://localhost:5174' }, + env: { ...process.env, VITE_DEV_SERVER_URL: DEV_URL }, }); elec.on('exit', (code) => { vite.kill(); process.exit(code ?? 0); }); + +vite.on('exit', (code) => { + if (code && code !== 0) { + console.error('Vite exited with code', code); + elec.kill(); + process.exit(code); + } +}); diff --git a/vite.config.ts b/vite.config.ts index c6aa301..94c6cb5 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -11,6 +11,7 @@ export default defineConfig({ emptyOutDir: true, }, server: { + host: '127.0.0.1', port: 5174, strictPort: true, },