Fix black screen: wait for Vite before Electron, retry loadURL, show boot fallback.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ivan Fontosh
2026-09-14 07:39:46 +08:00
parent 45105cd069
commit df50b6b680
6 changed files with 102 additions and 10 deletions
+29 -1
View File
@@ -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; const devUrl = process.env.VITE_DEV_SERVER_URL;
if (devUrl) { 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,<h1 style="color:#fff;font-family:sans-serif;padding:24px">Dev server not reachable: ${devUrl}</h1>`,
);
}
};
void load();
} else { } else {
void mainWindow.loadFile(path.join(__dirname, '../renderer/index.html')); void mainWindow.loadFile(path.join(__dirname, '../renderer/index.html'));
} }
+11 -1
View File
@@ -155,12 +155,22 @@ export function App() {
void (async () => { void (async () => {
if (!window.ttrpgBuilder) { if (!window.ttrpgBuilder) {
setBooting(false); setBooting(false);
setStatus(
locale === 'ru'
? 'Нет связи с Electron preload (перезапустите npm run dev)'
: 'Electron preload missing (restart npm run dev)',
);
return; return;
} }
try {
await window.ttrpgBuilder.seedPresets(); await window.ttrpgBuilder.seedPresets();
await refresh(); await refresh();
setBooting(false);
setStatus(locale === 'ru' ? 'Пресеты готовы' : 'Presets ready'); setStatus(locale === 'ru' ? 'Пресеты готовы' : 'Presets ready');
} catch (e) {
setStatus(String(e));
} finally {
setBooting(false);
}
})(); })();
}, [refresh, locale]); }, [refresh, locale]);
+5 -1
View File
@@ -4,9 +4,13 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>TTRPG Character Builder</title> <title>TTRPG Character Builder</title>
<style>
html, body { margin: 0; height: 100%; background: #09090b; color: #eee; font-family: Segoe UI, sans-serif; }
#boot-fallback { padding: 24px; }
</style>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"><div id="boot-fallback">Loading TTRPG Character Builder…</div></div>
<script type="module" src="./main.tsx"></script> <script type="module" src="./main.tsx"></script>
</body> </body>
</html> </html>
+18 -1
View File
@@ -2,4 +2,21 @@ import { createRoot } from 'react-dom/client';
import { App } from './App.tsx'; import { App } from './App.tsx';
import './styles.css'; import './styles.css';
createRoot(document.getElementById('root')!).render(<App />); const rootEl = document.getElementById('root');
if (!rootEl) {
document.body.innerHTML = '<pre style="color:#f66;padding:24px">#root missing</pre>';
} else {
try {
createRoot(rootEl).render(<App />);
} catch (err) {
rootEl.innerHTML = `<pre style="color:#f66;padding:24px;white-space:pre-wrap">${String(err)}</pre>`;
console.error(err);
}
}
window.addEventListener('error', (e) => {
console.error('window.error', e.error || e.message);
});
window.addEventListener('unhandledrejection', (e) => {
console.error('unhandledrejection', e.reason);
});
+35 -3
View File
@@ -3,9 +3,11 @@ import { spawn } from 'node:child_process';
import path from 'node:path'; import path from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import fs from 'node:fs'; import fs from 'node:fs';
import http from 'node:http';
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __dirname = path.dirname(fileURLToPath(import.meta.url));
const root = path.resolve(__dirname, '..'); const root = path.resolve(__dirname, '..');
const DEV_URL = 'http://127.0.0.1:5174/';
async function buildMain() { async function buildMain() {
await build({ await build({
@@ -35,24 +37,54 @@ function electronPath() {
return path.join(root, 'node_modules', 'electron', 'cli.js'); 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(); await buildMain();
const vite = spawn( const vite = spawn(
process.platform === 'win32' ? 'npx.cmd' : 'npx', 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 }, { 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(), ['.'], { const elec = spawn(electronPath(), ['.'], {
cwd: root, cwd: root,
stdio: 'inherit', stdio: 'inherit',
shell: true, 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) => { elec.on('exit', (code) => {
vite.kill(); vite.kill();
process.exit(code ?? 0); process.exit(code ?? 0);
}); });
vite.on('exit', (code) => {
if (code && code !== 0) {
console.error('Vite exited with code', code);
elec.kill();
process.exit(code);
}
});
+1
View File
@@ -11,6 +11,7 @@ export default defineConfig({
emptyOutDir: true, emptyOutDir: true,
}, },
server: { server: {
host: '127.0.0.1',
port: 5174, port: 5174,
strictPort: true, strictPort: true,
}, },