@@ -4,59 +4,138 @@ import {
44 ipcMain ,
55 shell ,
66 dialog ,
7- protocol ,
87} from "electron" ;
98import * as path from "node:path" ;
9+ import * as http from "node:http" ;
10+ import * as fs from "node:fs" ;
11+ import * as net from "node:net" ;
1012import {
1113 launchSidecar ,
1214 shutdownSidecar ,
1315 getSidecarPort ,
14- SidecarReady ,
15- SidecarOutput ,
16- SidecarExit ,
1716} from "./sidecar" ;
1817
1918const isDev = process . argv . includes ( "--dev" ) ;
2019const gotTheLock = app . requestSingleInstanceLock ( ) ;
2120
21+ if ( process . platform === "linux" ) {
22+ app . disableHardwareAcceleration ( ) ;
23+ }
24+
2225let mainWindow : BrowserWindow | null = null ;
2326let pendingAuthCallback : {
2427 success : boolean ;
2528 token ?: string ;
2629 error ?: string ;
2730} | null = null ;
28-
29- function getFrontendPath ( ) : string {
30- if ( isDev ) {
31- return "http://127.0.0.1:3000" ;
31+ let staticServer : http . Server | null = null ;
32+
33+ function findFrontendDir ( ) : string {
34+ const candidates = [
35+ path . join ( __dirname , "../frontend" ) ,
36+ path . join ( __dirname , "../../desktop-ui/out" ) ,
37+ ] ;
38+ for ( const dir of candidates ) {
39+ if ( fs . existsSync ( path . join ( dir , "index.html" ) ) ) {
40+ return dir ;
41+ }
3242 }
33- return path . join ( __dirname , "../frontend/index.html" ) ;
43+ throw new Error (
44+ `Frontend build not found. Tried: ${ candidates . join ( ", " ) } . ` +
45+ `Run pnpm --filter @openlinear/desktop-ui build:electron first.`
46+ ) ;
47+ }
48+
49+ async function pickFreePort ( ) : Promise < number > {
50+ return new Promise ( ( resolve , reject ) => {
51+ const srv = net . createServer ( ) ;
52+ srv . listen ( 0 , "127.0.0.1" , ( ) => {
53+ const addr = srv . address ( ) ;
54+ if ( addr && typeof addr === "object" && addr . port ) {
55+ const port = addr . port ;
56+ srv . close ( ( ) => resolve ( port ) ) ;
57+ } else {
58+ srv . close ( ( ) => reject ( new Error ( "Failed to get ephemeral port" ) ) ) ;
59+ }
60+ } ) ;
61+ srv . on ( "error" , ( err ) => reject ( err ) ) ;
62+ } ) ;
63+ }
64+
65+ async function startStaticServer ( ) : Promise < number > {
66+ if ( isDev ) return 3000 ;
67+ const staticDir = findFrontendDir ( ) ;
68+ const port = await pickFreePort ( ) ;
69+
70+ staticServer = http . createServer ( ( req , res ) => {
71+ const reqPath = decodeURIComponent ( req . url || "/" ) ;
72+ let filePath = path . join ( staticDir , reqPath ) ;
73+ if ( ! fs . existsSync ( filePath ) || fs . statSync ( filePath ) . isDirectory ( ) ) {
74+ filePath = path . join ( staticDir , "index.html" ) ;
75+ }
76+ fs . readFile ( filePath , ( err , data ) => {
77+ if ( err ) {
78+ res . writeHead ( 404 ) ;
79+ res . end ( "Not found" ) ;
80+ return ;
81+ }
82+ const ext = path . extname ( filePath ) ;
83+ const mime : Record < string , string > = {
84+ ".html" : "text/html" ,
85+ ".js" : "application/javascript" ,
86+ ".css" : "text/css" ,
87+ ".json" : "application/json" ,
88+ ".png" : "image/png" ,
89+ ".jpg" : "image/jpeg" ,
90+ ".svg" : "image/svg+xml" ,
91+ ".woff2" : "font/woff2" ,
92+ } ;
93+ res . writeHead ( 200 , { "Content-Type" : mime [ ext ] || "application/octet-stream" } ) ;
94+ res . end ( data ) ;
95+ } ) ;
96+ } ) ;
97+
98+ return new Promise ( ( resolve , reject ) => {
99+ staticServer ! . listen ( port , "127.0.0.1" , ( ) => {
100+ console . log ( `[Static] Serving ${ staticDir } on http://127.0.0.1:${ port } ` ) ;
101+ resolve ( port ) ;
102+ } ) ;
103+ staticServer ! . on ( "error" , reject ) ;
104+ } ) ;
34105}
35106
36- function createWindow ( ) : BrowserWindow {
107+ function createWindow ( url : string ) : BrowserWindow {
37108 const win = new BrowserWindow ( {
38109 width : 1200 ,
39110 height : 800 ,
40111 minWidth : 800 ,
41112 minHeight : 500 ,
42113 center : true ,
43- frame : false ,
44- titleBarStyle : "hidden" ,
114+ show : false ,
115+ backgroundColor : "#0a0a0a" ,
116+ frame : process . platform === "darwin" ? false : true ,
117+ titleBarStyle : process . platform === "darwin" ? "hidden" : "default" ,
45118 webPreferences : {
46119 preload : path . join ( __dirname , "preload.js" ) ,
47120 contextIsolation : true ,
48121 nodeIntegration : false ,
49122 sandbox : true ,
50123 allowRunningInsecureContent : false ,
51124 experimentalFeatures : false ,
125+ offscreen : false ,
52126 } ,
53127 } ) ;
54128
55- const frontendPath = getFrontendPath ( ) ;
56- if ( frontendPath . startsWith ( "http" ) ) {
57- win . loadURL ( frontendPath ) ;
58- } else {
59- win . loadFile ( frontendPath ) ;
129+ win . loadURL ( url ) ;
130+
131+ win . once ( "ready-to-show" , ( ) => {
132+ win . show ( ) ;
133+ win . focus ( ) ;
134+ console . log ( "[Window] Ready and shown" ) ;
135+ } ) ;
136+
137+ if ( isDev ) {
138+ win . webContents . openDevTools ( ) ;
60139 }
61140
62141 return win ;
@@ -70,7 +149,7 @@ function emitToWindow(channel: string, payload: unknown): void {
70149
71150function focusMainWindow ( ) : void {
72151 if ( ! mainWindow || mainWindow . isDestroyed ( ) ) {
73- mainWindow = createWindow ( ) ;
152+ return ;
74153 }
75154 if ( mainWindow . isMinimized ( ) ) mainWindow . restore ( ) ;
76155 mainWindow . show ( ) ;
@@ -120,7 +199,13 @@ app.whenReady().then(async () => {
120199 app . setAsDefaultProtocolClient ( "openlinear" ) ;
121200 }
122201
123- mainWindow = createWindow ( ) ;
202+ const staticPort = await startStaticServer ( ) ;
203+ const frontendUrl = isDev
204+ ? "http://127.0.0.1:3000"
205+ : `http://127.0.0.1:${ staticPort } ` ;
206+
207+ console . log ( `[App] Loading frontend from ${ frontendUrl } ` ) ;
208+ mainWindow = createWindow ( frontendUrl ) ;
124209
125210 try {
126211 const port = await launchSidecar ( emitToWindow ) ;
@@ -136,12 +221,16 @@ app.whenReady().then(async () => {
136221
137222 app . on ( "activate" , ( ) => {
138223 if ( BrowserWindow . getAllWindows ( ) . length === 0 ) {
139- mainWindow = createWindow ( ) ;
224+ mainWindow = createWindow ( frontendUrl ) ;
140225 }
141226 } ) ;
142227} ) ;
143228
144229app . on ( "window-all-closed" , ( ) => {
230+ if ( staticServer ) {
231+ staticServer . close ( ) ;
232+ staticServer = null ;
233+ }
145234 if ( process . platform !== "darwin" ) {
146235 shutdownSidecar ( ) ;
147236 app . quit ( ) ;
@@ -150,6 +239,10 @@ app.on("window-all-closed", () => {
150239
151240app . on ( "before-quit" , ( ) => {
152241 shutdownSidecar ( ) ;
242+ if ( staticServer ) {
243+ staticServer . close ( ) ;
244+ staticServer = null ;
245+ }
153246} ) ;
154247
155248app . on ( "open-url" , ( _event , url ) => {
@@ -234,10 +327,10 @@ ipcMain.handle("get-arch", () => {
234327const storeCache = new Map < string , Map < string , unknown > > ( ) ;
235328
236329ipcMain . handle ( "store-load" , async ( _event , filename : string ) => {
237- const fs = await import ( "node:fs/promises" ) ;
330+ const fsPromises = await import ( "node:fs/promises" ) ;
238331 const storePath = path . join ( app . getPath ( "userData" ) , filename ) ;
239332 try {
240- const data = await fs . readFile ( storePath , "utf-8" ) ;
333+ const data = await fsPromises . readFile ( storePath , "utf-8" ) ;
241334 const parsed = JSON . parse ( data ) as Record < string , unknown > ;
242335 storeCache . set ( filename , new Map ( Object . entries ( parsed ) ) ) ;
243336 } catch {
@@ -271,12 +364,12 @@ ipcMain.handle(
271364) ;
272365
273366ipcMain . handle ( "store-save" , async ( _event , filename : string ) => {
274- const fs = await import ( "node:fs/promises" ) ;
367+ const fsPromises = await import ( "node:fs/promises" ) ;
275368 const store = storeCache . get ( filename ) ;
276369 if ( ! store ) return ;
277370 const storePath = path . join ( app . getPath ( "userData" ) , filename ) ;
278371 const data = Object . fromEntries ( store . entries ( ) ) ;
279- await fs . writeFile ( storePath , JSON . stringify ( data , null , 2 ) , "utf-8" ) ;
372+ await fsPromises . writeFile ( storePath , JSON . stringify ( data , null , 2 ) , "utf-8" ) ;
280373} ) ;
281374
282375ipcMain . handle ( "consume_pending_auth_callback" , async ( ) => {
0 commit comments