diff --git a/gitnexus-desktop/package.json b/gitnexus-desktop/package.json index 78e16c102..3b6e99e0f 100644 --- a/gitnexus-desktop/package.json +++ b/gitnexus-desktop/package.json @@ -11,6 +11,7 @@ "node": ">=20.0.0" }, "scripts": { + "predev": "node scripts/ensure-gitnexus-runtime.mjs", "dev": "electron-vite dev --outDir=dist", "start": "electron-vite preview --outDir=dist", "bundle": "electron-vite build --outDir=dist", diff --git a/gitnexus-desktop/scripts/ensure-gitnexus-runtime.mjs b/gitnexus-desktop/scripts/ensure-gitnexus-runtime.mjs new file mode 100644 index 000000000..31a74798b --- /dev/null +++ b/gitnexus-desktop/scripts/ensure-gitnexus-runtime.mjs @@ -0,0 +1,39 @@ +import { spawnSync } from 'node:child_process'; +import { existsSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const workspaceRoot = path.resolve(__dirname, '..', '..'); +const sharedRoot = path.join(workspaceRoot, 'gitnexus-shared'); +const gitnexusRoot = path.join(workspaceRoot, 'gitnexus'); +const gitnexusServerEntry = path.join(gitnexusRoot, 'dist', 'server', 'api.js'); + +const runNpm = (args, cwd) => { + const result = spawnSync('npm', args, { + cwd, + stdio: 'inherit', + shell: process.platform === 'win32', + }); + + if (result.error) { + throw result.error; + } + + if (result.status !== 0) { + process.exit(result.status ?? 1); + } +}; + +if (!existsSync(path.join(sharedRoot, 'node_modules', 'typescript'))) { + runNpm(['ci'], sharedRoot); +} + +if (!existsSync(path.join(gitnexusRoot, 'node_modules', 'tsx'))) { + runNpm(['ci'], gitnexusRoot); +} + +if (!existsSync(gitnexusServerEntry)) { + runNpm(['run', 'build'], gitnexusRoot); +} diff --git a/gitnexus-desktop/src/main/main.ts b/gitnexus-desktop/src/main/main.ts index 113633b29..ff4abd7d7 100644 --- a/gitnexus-desktop/src/main/main.ts +++ b/gitnexus-desktop/src/main/main.ts @@ -1,15 +1,76 @@ -import { app, BrowserWindow } from 'electron'; +import { app, BrowserWindow, dialog } from 'electron'; import path from 'node:path'; -import { fileURLToPath } from 'node:url'; +import { fileURLToPath, pathToFileURL } from 'node:url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); +const GITNEXUS_PORT = 4747; +const GITNEXUS_HOST = 'localhost'; +const GITNEXUS_SERVER_ENTRY = path.resolve(__dirname, '../../../gitnexus/dist/server/api.js'); + +type GitNexusServerModule = { + createServer: (port: number, host?: string) => Promise; +}; + +let serverReadyPromise: Promise | null = null; + +const getErrorMessage = (error: unknown): string => { + if (error instanceof Error) { + return error.message; + } + + return String(error); +}; + +const isAddressInUseError = (error: unknown): boolean => { + return (error as NodeJS.ErrnoException | undefined)?.code === 'EADDRINUSE'; +}; + +const showStartupError = (error: unknown): void => { + if (isAddressInUseError(error)) { + dialog.showErrorBox( + 'GitNexus Desktop Startup Failed', + `Port ${GITNEXUS_PORT} is already in use.\n\nStop the other process using port ${GITNEXUS_PORT} and try again.`, + ); + return; + } + + dialog.showErrorBox( + 'GitNexus Desktop Startup Failed', + `Failed to start GitNexus Desktop.\n\n${getErrorMessage(error)}`, + ); +}; + +const loadGitNexusServerModule = async (): Promise => { + try { + return (await import(pathToFileURL(GITNEXUS_SERVER_ENTRY).href)) as GitNexusServerModule; + } catch (error) { + throw new Error( + `Could not load the GitNexus server runtime at ${GITNEXUS_SERVER_ENTRY}. ${getErrorMessage(error)}`, + ); + } +}; + +const ensureGitNexusServerStarted = async (): Promise => { + if (!serverReadyPromise) { + serverReadyPromise = (async () => { + const { createServer } = await loadGitNexusServerModule(); + await createServer(GITNEXUS_PORT, GITNEXUS_HOST); + })().catch((error) => { + serverReadyPromise = null; + throw error; + }); + } + + await serverReadyPromise; +}; async function createWindow(): Promise { const mainWindow = new BrowserWindow({ width: 1280, height: 800, title: 'GitNexus Desktop', + show: false, webPreferences: { preload: path.join(__dirname, '../preload/preload.mjs'), contextIsolation: true, @@ -17,6 +78,12 @@ async function createWindow(): Promise { }, }); + mainWindow.once('ready-to-show', () => { + if (!mainWindow.isDestroyed()) { + mainWindow.show(); + } + }); + const rendererUrl = process.env.ELECTRON_RENDERER_URL; if (rendererUrl) { @@ -27,12 +94,22 @@ async function createWindow(): Promise { await mainWindow.loadFile(path.join(__dirname, '../renderer/src/renderer/index.html')); } -app.whenReady().then(() => { - void createWindow(); +app.whenReady().then(async () => { + try { + await ensureGitNexusServerStarted(); + await createWindow(); + } catch (error) { + showStartupError(error); + app.quit(); + return; + } app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { - void createWindow(); + void createWindow().catch((error) => { + showStartupError(error); + app.quit(); + }); } }); });