|
| 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(); |
0 commit comments