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:
+29
-1
@@ -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
@@ -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]);
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user