f4c0ac1438
Nested NPC groups with color, graph filter, and scene/storyline binding; Foundry worlds/modules import actors into groups; storyline merge asks on NPC name conflicts and reports NPC counts. Co-authored-by: Cursor <cursoragent@cursor.com>
187 lines
4.8 KiB
JavaScript
187 lines
4.8 KiB
JavaScript
import { context } from 'esbuild';
|
||
import path from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
import { spawn } from 'node:child_process';
|
||
import http from 'node:http';
|
||
|
||
const __filename = fileURLToPath(import.meta.url);
|
||
const __dirname = path.dirname(__filename);
|
||
const root = path.resolve(__dirname, '..');
|
||
|
||
const electronEnv = {
|
||
...process.env,
|
||
NODE_ENV: 'development',
|
||
DND_SKIP_LICENSE: process.env.DND_SKIP_LICENSE ?? '1',
|
||
// Совпадает с vite.config.ts `server.host` (не localhost: на Windows он часто → ::1).
|
||
VITE_DEV_SERVER_URL: 'http://127.0.0.1:5173/',
|
||
};
|
||
|
||
function spawnShell(command, opts = {}) {
|
||
const child = spawn(command, {
|
||
cwd: root,
|
||
stdio: 'inherit',
|
||
shell: true,
|
||
...opts,
|
||
});
|
||
child.on('exit', (code) => {
|
||
if (code !== 0 && code !== null) {
|
||
process.exitCode = code;
|
||
}
|
||
});
|
||
return child;
|
||
}
|
||
|
||
/** Убивает дерево процессов (на Windows `child.kill()` часто не гасит Vite на 5173). */
|
||
function killTree(child) {
|
||
if (!child || child.killed) return;
|
||
if (typeof child.exitCode === 'number' && child.exitCode !== null) return;
|
||
if (process.platform === 'win32' && child.pid) {
|
||
spawn('taskkill', ['/PID', String(child.pid), '/T', '/F'], {
|
||
stdio: 'ignore',
|
||
windowsHide: true,
|
||
detached: true,
|
||
});
|
||
} else {
|
||
try {
|
||
child.kill('SIGTERM');
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
}
|
||
|
||
function waitForVite(url = 'http://127.0.0.1:5173/editor.html', timeoutMs = 60000) {
|
||
const started = Date.now();
|
||
return new Promise((resolve, reject) => {
|
||
const tick = () => {
|
||
const req = http.get(url, (res) => {
|
||
res.resume();
|
||
resolve();
|
||
});
|
||
req.on('error', () => {
|
||
if (Date.now() - started > timeoutMs) {
|
||
reject(new Error(`Timed out waiting for Vite at ${url}`));
|
||
return;
|
||
}
|
||
setTimeout(tick, 300);
|
||
});
|
||
req.setTimeout(2000, () => {
|
||
req.destroy();
|
||
});
|
||
};
|
||
tick();
|
||
});
|
||
}
|
||
|
||
let shuttingDown = false;
|
||
let electron = null;
|
||
let vite = null;
|
||
let dispose = async () => {};
|
||
let restartTimer = null;
|
||
let electronStarted = false;
|
||
let restartingElectron = false;
|
||
|
||
async function shutdown() {
|
||
if (shuttingDown) return;
|
||
shuttingDown = true;
|
||
if (restartTimer) clearTimeout(restartTimer);
|
||
killTree(vite);
|
||
killTree(electron);
|
||
await dispose();
|
||
process.exit(0);
|
||
}
|
||
|
||
function startElectron() {
|
||
if (shuttingDown) return;
|
||
restartingElectron = false;
|
||
if (electron) killTree(electron);
|
||
electron = spawnShell('npx electron .', { env: electronEnv });
|
||
electron.once('exit', () => {
|
||
if (restartingElectron || shuttingDown) return;
|
||
void shutdown();
|
||
});
|
||
electronStarted = true;
|
||
}
|
||
|
||
function scheduleElectronRestart() {
|
||
if (shuttingDown || !electronStarted) return;
|
||
if (restartTimer) clearTimeout(restartTimer);
|
||
restartTimer = setTimeout(() => {
|
||
restartTimer = null;
|
||
console.log('[dev] main/preload rebuilt — restarting Electron…');
|
||
restartingElectron = true;
|
||
startElectron();
|
||
}, 300);
|
||
}
|
||
|
||
function createRestartPlugin() {
|
||
return {
|
||
name: 'restart-electron-on-rebuild',
|
||
setup(build) {
|
||
build.onEnd((result) => {
|
||
if (result.errors.length === 0) scheduleElectronRestart();
|
||
});
|
||
},
|
||
};
|
||
}
|
||
|
||
async function watchMainAndPreload() {
|
||
const restartPlugin = createRestartPlugin();
|
||
const main = await context({
|
||
entryPoints: [path.join(root, 'app/main/index.ts')],
|
||
outfile: path.join(root, 'dist/main/index.cjs'),
|
||
platform: 'node',
|
||
target: 'node22',
|
||
format: 'cjs',
|
||
bundle: true,
|
||
sourcemap: true,
|
||
external: ['electron', 'electron-updater', 'sharp', 'ffmpeg-static', 'classic-level'],
|
||
define: { 'process.env.NODE_ENV': JSON.stringify('development') },
|
||
plugins: [restartPlugin],
|
||
});
|
||
await main.rebuild();
|
||
await main.watch();
|
||
|
||
const preload = await context({
|
||
entryPoints: [path.join(root, 'app/preload/index.ts')],
|
||
outfile: path.join(root, 'dist/preload/index.cjs'),
|
||
platform: 'node',
|
||
target: 'node22',
|
||
format: 'cjs',
|
||
bundle: true,
|
||
sourcemap: true,
|
||
external: ['electron'],
|
||
define: { 'process.env.NODE_ENV': JSON.stringify('development') },
|
||
plugins: [restartPlugin],
|
||
});
|
||
await preload.rebuild();
|
||
await preload.watch();
|
||
|
||
return async () => {
|
||
await Promise.all([main.dispose(), preload.dispose()]);
|
||
};
|
||
}
|
||
|
||
dispose = await watchMainAndPreload();
|
||
|
||
vite = spawnShell('npx vite dev --strictPort', {
|
||
env: electronEnv,
|
||
});
|
||
|
||
try {
|
||
await waitForVite();
|
||
} catch (err) {
|
||
console.error('[dev] Vite did not start in time:', err);
|
||
}
|
||
|
||
startElectron();
|
||
|
||
process.on('SIGINT', () => void shutdown());
|
||
process.on('SIGTERM', () => void shutdown());
|
||
|
||
vite.once('exit', (code) => {
|
||
if (code !== 0 && code !== null && !shuttingDown) {
|
||
void shutdown();
|
||
}
|
||
});
|