Skip to content

Commit 4616324

Browse files
Add desktop electron main and preload processes
1 parent 8e533b1 commit 4616324

15 files changed

Lines changed: 725 additions & 0 deletions

File tree

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
/// <reference types="vite/client" />
2+
import { createRequestHandler } from '@remix-run/node';
3+
import electron, { app, BrowserWindow, ipcMain, protocol, session } from 'electron';
4+
import log from 'electron-log';
5+
import path from 'node:path';
6+
import * as pkg from '../../package.json';
7+
import { setupAutoUpdater } from './utils/auto-update';
8+
import { isDev, DEFAULT_PORT } from './utils/constants';
9+
import { initViteServer, viteServer } from './utils/vite-server';
10+
import { setupMenu } from './ui/menu';
11+
import { createWindow } from './ui/window';
12+
import { initCookies, storeCookies } from './utils/cookie';
13+
import { loadServerBuild, serveAsset } from './utils/serve';
14+
import { reloadOnChange } from './utils/reload';
15+
16+
Object.assign(console, log.functions);
17+
18+
console.debug('main: import.meta.env:', import.meta.env);
19+
console.log('main: isDev:', isDev);
20+
console.log('NODE_ENV:', global.process.env.NODE_ENV);
21+
console.log('isPackaged:', app.isPackaged);
22+
23+
// Log unhandled errors
24+
process.on('uncaughtException', async (error) => {
25+
console.log('Uncaught Exception:', error);
26+
});
27+
28+
process.on('unhandledRejection', async (error) => {
29+
console.log('Unhandled Rejection:', error);
30+
});
31+
32+
(() => {
33+
const root = global.process.env.APP_PATH_ROOT ?? import.meta.env.VITE_APP_PATH_ROOT;
34+
35+
if (root === undefined) {
36+
console.log('no given APP_PATH_ROOT or VITE_APP_PATH_ROOT. default path is used.');
37+
return;
38+
}
39+
40+
if (!path.isAbsolute(root)) {
41+
console.log('APP_PATH_ROOT must be absolute path.');
42+
global.process.exit(1);
43+
}
44+
45+
console.log(`APP_PATH_ROOT: ${root}`);
46+
47+
const subdirName = pkg.name;
48+
49+
for (const [key, val] of [
50+
['appData', ''],
51+
['userData', subdirName],
52+
['sessionData', subdirName],
53+
] as const) {
54+
app.setPath(key, path.join(root, val));
55+
}
56+
57+
app.setAppLogsPath(path.join(root, subdirName, 'Logs'));
58+
})();
59+
60+
console.log('appPath:', app.getAppPath());
61+
62+
const keys: Parameters<typeof app.getPath>[number][] = ['home', 'appData', 'userData', 'sessionData', 'logs', 'temp'];
63+
keys.forEach((key) => console.log(`${key}:`, app.getPath(key)));
64+
console.log('start whenReady');
65+
66+
declare global {
67+
// eslint-disable-next-line @typescript-eslint/naming-convention
68+
var __electron__: typeof electron;
69+
}
70+
71+
(async () => {
72+
await app.whenReady();
73+
console.log('App is ready');
74+
75+
// Load any existing cookies from ElectronStore, set as cookie
76+
await initCookies();
77+
78+
const serverBuild = await loadServerBuild();
79+
80+
protocol.handle('http', async (req) => {
81+
console.log('Handling request for:', req.url);
82+
83+
if (isDev) {
84+
console.log('Dev mode: forwarding to vite server');
85+
return await fetch(req);
86+
}
87+
88+
req.headers.append('Referer', req.referrer);
89+
90+
try {
91+
const url = new URL(req.url);
92+
93+
// Forward requests to specific local server ports
94+
if (url.port !== `${DEFAULT_PORT}`) {
95+
console.log('Forwarding request to local server:', req.url);
96+
return await fetch(req);
97+
}
98+
99+
// Always try to serve asset first
100+
const assetPath = path.join(app.getAppPath(), 'build', 'client');
101+
const res = await serveAsset(req, assetPath);
102+
103+
if (res) {
104+
console.log('Served asset:', req.url);
105+
return res;
106+
}
107+
108+
// Forward all cookies to remix server
109+
const cookies = await session.defaultSession.cookies.get({});
110+
111+
if (cookies.length > 0) {
112+
req.headers.set('Cookie', cookies.map((c) => `${c.name}=${c.value}`).join('; '));
113+
114+
// Store all cookies
115+
await storeCookies(cookies);
116+
}
117+
118+
// Create request handler with the server build
119+
const handler = createRequestHandler(serverBuild, 'production');
120+
console.log('Handling request with server build:', req.url);
121+
122+
const result = await handler(req, {
123+
/*
124+
* Remix app access cloudflare.env
125+
* Need to pass an empty object to prevent undefined
126+
*/
127+
// @ts-ignore:next-line
128+
cloudflare: {},
129+
});
130+
131+
return result;
132+
} catch (err) {
133+
console.log('Error handling request:', {
134+
url: req.url,
135+
error:
136+
err instanceof Error
137+
? {
138+
message: err.message,
139+
stack: err.stack,
140+
cause: err.cause,
141+
}
142+
: err,
143+
});
144+
145+
const error = err instanceof Error ? err : new Error(String(err));
146+
147+
return new Response(`Error handling request to ${req.url}: ${error.stack ?? error.message}`, {
148+
status: 500,
149+
headers: { 'content-type': 'text/plain' },
150+
});
151+
}
152+
});
153+
154+
const rendererURL = await (isDev
155+
? (async () => {
156+
await initViteServer();
157+
158+
if (!viteServer) {
159+
throw new Error('Vite server is not initialized');
160+
}
161+
162+
const listen = await viteServer.listen();
163+
global.__electron__ = electron;
164+
viteServer.printUrls();
165+
166+
return `http://localhost:${listen.config.server.port}`;
167+
})()
168+
: `http://localhost:${DEFAULT_PORT}`);
169+
170+
console.log('Using renderer URL:', rendererURL);
171+
172+
const win = await createWindow(rendererURL);
173+
174+
app.on('activate', async () => {
175+
if (BrowserWindow.getAllWindows().length === 0) {
176+
await createWindow(rendererURL);
177+
}
178+
});
179+
180+
console.log('end whenReady');
181+
182+
return win;
183+
})()
184+
.then((win) => {
185+
// IPC samples : send and recieve.
186+
let count = 0;
187+
setInterval(() => win.webContents.send('ping', `hello from main! ${count++}`), 60 * 1000);
188+
ipcMain.handle('ipcTest', (event, ...args) => console.log('ipc: renderer -> main', { event, ...args }));
189+
190+
return win;
191+
})
192+
.then((win) => setupMenu(win));
193+
194+
app.on('window-all-closed', () => {
195+
if (process.platform !== 'darwin') {
196+
app.quit();
197+
}
198+
});
199+
200+
reloadOnChange();
201+
setupAutoUpdater();
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"include": ["."],
3+
"compilerOptions": {
4+
"lib": ["ESNext"],
5+
"jsx": "preserve",
6+
"target": "ESNext",
7+
"noEmit": true,
8+
"skipLibCheck": true,
9+
"useDefineForClassFields": true,
10+
11+
/* modules */
12+
"moduleResolution": "Bundler",
13+
"allowImportingTsExtensions": true,
14+
"resolveJsonModule": true,
15+
"module": "ESNext",
16+
"isolatedModules": true,
17+
"emitDeclarationOnly": true,
18+
"declaration": true,
19+
"declarationDir": "./dist",
20+
21+
/* type checking */
22+
"strict": true,
23+
"noUnusedLocals": true,
24+
"noUnusedParameters": true,
25+
"noFallthroughCasesInSwitch": true,
26+
"noImplicitReturns": true,
27+
"verbatimModuleSyntax": true,
28+
"forceConsistentCasingInFileNames": true
29+
}
30+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { BrowserWindow, Menu } from 'electron';
2+
3+
export function setupMenu(win: BrowserWindow): void {
4+
const app = Menu.getApplicationMenu();
5+
Menu.setApplicationMenu(
6+
Menu.buildFromTemplate([
7+
...(app ? app.items : []),
8+
{
9+
label: 'Go',
10+
submenu: [
11+
{
12+
label: 'Back',
13+
accelerator: 'CmdOrCtrl+[',
14+
click: () => {
15+
win?.webContents.navigationHistory.goBack();
16+
},
17+
},
18+
{
19+
label: 'Forward',
20+
accelerator: 'CmdOrCtrl+]',
21+
click: () => {
22+
win?.webContents.navigationHistory.goForward();
23+
},
24+
},
25+
],
26+
},
27+
]),
28+
);
29+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { app, BrowserWindow } from 'electron';
2+
import path from 'node:path';
3+
import { isDev } from '../utils/constants';
4+
import { store } from '../utils/store';
5+
6+
export function createWindow(rendererURL: string) {
7+
console.log('Creating window with URL:', rendererURL);
8+
9+
const bounds = store.get('bounds');
10+
console.log('restored bounds:', bounds);
11+
12+
// preload path
13+
const preloadPath = path.join(isDev ? process.cwd() : app.getAppPath(), 'build', 'electron', 'preload', 'index.cjs');
14+
15+
const win = new BrowserWindow({
16+
...{
17+
width: 1200,
18+
height: 800,
19+
...bounds,
20+
},
21+
vibrancy: 'under-window',
22+
visualEffectState: 'active',
23+
webPreferences: {
24+
preload: preloadPath,
25+
},
26+
});
27+
28+
console.log('Window created, loading URL...');
29+
win.loadURL(rendererURL).catch((err) => {
30+
console.log('Failed to load URL:', err);
31+
});
32+
33+
win.webContents.on('did-fail-load', (_, errorCode, errorDescription) => {
34+
console.log('Failed to load:', errorCode, errorDescription);
35+
});
36+
37+
win.webContents.on('did-finish-load', () => {
38+
console.log('Window finished loading');
39+
});
40+
41+
// Open devtools in development
42+
if (isDev) {
43+
win.webContents.openDevTools();
44+
}
45+
46+
const boundsListener = () => {
47+
const bounds = win.getBounds();
48+
store.set('bounds', bounds);
49+
};
50+
win.on('moved', boundsListener);
51+
win.on('resized', boundsListener);
52+
53+
return win;
54+
}

0 commit comments

Comments
 (0)