Files
DndGamePlayer/scripts/dev.mjs
T
Ivan Fontosh c8ab9dd567 feat(editor): побочные сюжетные линии и импорт/экспорт линий
Добавлена полноценная поддержка побочных сюжетных линий в редакторе и на
пульте: визуальное выделение компонент графа, запрет недопустимых связей
между основной и побочными линиями, метки «ПОБОЧНАЯ» и названия линий.

Реализован partial export/import сюжетных линий:
- экспорт выбранных линий в урезанный .ttrpg.zip с manifest в project.json;
- импорт в открытый проект с выбором линий, разрешением конфликтов названий
  сцен (одна модалка на операцию) и отчётом о результате;
- новое окно источника импорта: «Из проекта» (dropdown, без текущего) или
  «Из файла»; на главном экране — только полный импорт из файла.

Исправлены гонки при открытии/закрытии проекта (сериализация open/close в
main, сброс зависших состояний UI), залипание оверлея прогресса экспорта и
старт Electron в dev после готовности Vite.

Добавлены unit-тесты для lineage, export/import и контрактов zipStore.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-09 14:47:28 +08:00

186 lines
4.6 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_DEV_SERVER_URL: 'http://localhost: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'],
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();
}
});