Skip to content

Commit 8a84e5c

Browse files
committed
feat: Add --browser-mode, --browser-mode-host, --browser-mode-port and --browser-mode-url startup arguments
refactor: Replaced bespoke argument parsing with node:util's parseArgs fix: Properly set isDev value in renderer refactor: Minify renderer in production refactor: Only use perf plugin in development
1 parent 5a68e7b commit 8a84e5c

File tree

9 files changed

+959
-172
lines changed

9 files changed

+959
-172
lines changed

package-lock.json

Lines changed: 797 additions & 17 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"@testing-library/user-event": "^14.6.1",
5353
"@types/dns-packet": "^5.6.5",
5454
"@types/eslint__js": "^8.42.3",
55+
"@types/express": "^5.0.5",
5556
"@types/follow-redirects": "1.8.0",
5657
"@types/fs-extra": "8.1.0",
5758
"@types/lodash": "^4.14.192",
@@ -85,6 +86,7 @@
8586
"eslint-plugin-jsdoc": "48.7.0",
8687
"eslint-plugin-react": "7.37.5",
8788
"eslint-plugin-react-hooks": "^7.0.0",
89+
"express": "^5.1.0",
8890
"fast-xml-parser": "3.21.1",
8991
"fs-extra": "8.1.0",
9092
"global-jsdom": "^27.0.0",

rsbuild.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export default defineConfig({
2828
output: {
2929
target: 'web',
3030
assetPrefix: 'auto',
31-
minify: false,
31+
minify: process.env.NODE_ENV === 'production',
3232
distPath: {
3333
root: './build/window',
3434
},

src/main/Main.ts

Lines changed: 69 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,18 @@ import { createErrorProxy } from '@shared/Util';
33
import { SocketClient } from '@shared/back/SocketClient';
44
import { BackIn, BackOut } from '@shared/back/types';
55
import { APP_TITLE } from '@shared/constants';
6+
import { num } from '@shared/utils/Coerce';
67
import { ChildProcess, fork } from 'child_process';
78
import * as electron from 'electron';
89
import { BrowserWindow, IpcMainEvent, app, dialog, ipcMain, session, shell } from 'electron';
9-
import installExtension, { REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS } from 'electron-devtools-installer';
1010
import { AppConfigData, AppPreferencesData } from 'flashpoint-launcher';
1111
import * as fs from 'fs-extra';
1212
import * as path from 'node:path';
1313
import { argv } from 'process';
1414
import { WebSocket } from 'ws';
1515
import * as Util from './Util';
1616
import { CustomIPC, WindowIPC } from './constants';
17+
import { createHeadlessServer } from './headless';
1718
import { Init } from './types';
1819

1920
const TIMEOUT_DELAY = 60_000;
@@ -29,11 +30,6 @@ const ALLOWED_HOSTS = [
2930
'cdn.discordapp.com',
3031
];
3132

32-
type LaunchOptions = {
33-
backend: boolean;
34-
frontend: boolean;
35-
}
36-
3733
type MainState = {
3834
window?: BrowserWindow;
3935
_installed?: boolean;
@@ -53,6 +49,12 @@ type MainState = {
5349
output: string;
5450
}
5551

52+
type LaunchComponents = {
53+
backend: boolean;
54+
browserFrontend: boolean;
55+
electronWindow: boolean;
56+
}
57+
5658
export function main(init: Init): void {
5759
const state: MainState = {
5860
window: undefined,
@@ -70,10 +72,19 @@ export function main(init: Init): void {
7072
output: '',
7173
};
7274

73-
startup({
75+
const launchComponents: LaunchComponents = {
7476
backend: !init.args['host-remote'],
75-
frontend: !init.args['back-only'],
76-
})
77+
browserFrontend: !!init.args['browser-mode'] && !init.args['back-only'],
78+
electronWindow: !init.args['browser-mode'] && !init.args['back-only'],
79+
};
80+
81+
if (!launchComponents.backend && !launchComponents.browserFrontend && !launchComponents.electronWindow) {
82+
console.log('Arguments for startup mean no components would run, exiting...');
83+
process.exit();
84+
return;
85+
}
86+
87+
startup(launchComponents)
7788
.catch((error) => {
7889
console.error(error);
7990
if (!Util.isDev) {
@@ -87,24 +98,9 @@ export function main(init: Init): void {
8798
app.quit();
8899
});
89100

90-
// Needed for modern Electron?
91-
// This is a workaround to ensure that the extension background workers are started
92-
// If you are updating Electron, confirm if this is still needed
93-
// https://github.com/electron/electron/issues/41613
94-
// function launchExtensionBackgroundWorkers( appSession = session.defaultSession ) {
95-
// return Promise.all(
96-
// appSession.getAllExtensions().map( async ( extension ) => {
97-
// const manifest = extension.manifest;
98-
// if ( manifest.manifest_version === 3 && manifest?.background?.service_worker ) {
99-
// await appSession.serviceWorkers.startWorkerForScope( extension.url );
100-
// }
101-
// } )
102-
// );
103-
// }
104-
105-
// -- Functions --
106-
107-
async function startup(opts: LaunchOptions) {
101+
async function startup(opts: LaunchComponents) {
102+
console.log(JSON.stringify(opts, undefined, 2));
103+
state.mainFolderPath = Util.getMainFolderPath();
108104
// app.disableHardwareAcceleration();
109105

110106
// Single process
@@ -114,14 +110,16 @@ export function main(init: Init): void {
114110
return;
115111
}
116112

117-
// Add app event listener(s)
118-
app.once('ready', onAppReady);
119-
app.once('window-all-closed', onAppWindowAllClosed);
120113
app.once('will-quit', onAppWillQuit);
114+
app.once('ready', onAppReady);
121115
app.on('web-contents-created', onAppWebContentsCreated);
122-
app.on('activate', onAppActivate);
123116
app.on('second-instance', onAppSecondInstance);
124117
app.on('open-url', onAppOpenUrl);
118+
app.on('activate', onAppActivate);
119+
app.commandLine.appendSwitch('ignore-connections-limit', 'localhost');
120+
121+
// Prevent closure on Mac after window closes
122+
app.once('window-all-closed', onAppWindowAllClosed);
125123

126124
// Add IPC event listener(s)
127125
ipcMain.on(InitRendererChannel, onInit);
@@ -202,29 +200,17 @@ export function main(init: Init): void {
202200
}
203201
});
204202

205-
// Add Socket event listener(s)
206-
state.socket.register(BackOut.QUIT, () => {
207-
state.isQuitting = true;
208-
state.socket.allowDeath();
209-
app.quit();
210-
});
211-
212-
app.commandLine.appendSwitch('ignore-connections-limit', 'localhost');
213-
214-
state.mainFolderPath = Util.getMainFolderPath();
215-
216203
// ---- Initialize ----
217-
// Load custom version text file
218-
await fs.promises.readFile(path.join(state.mainFolderPath, '.version'))
219-
.then((data) => {
220-
state._version = (data)
221-
? parseInt(data.toString().replace(/[^\d]/g, ''), 10) // (Remove all non-numerical characters, then parse it as a string)
222-
: -1; // (Version not found error code)
223-
})
224-
.catch(() => { /** No file, ignore */ });
225204

226205
// Start backend
227206
if (opts.backend) {
207+
// Exit Electron app if the backend quits
208+
state.socket.register(BackOut.QUIT, () => {
209+
state.isQuitting = true;
210+
state.socket.allowDeath();
211+
app.quit();
212+
});
213+
228214
await new Promise<void>((resolve, reject) => {
229215
// Fork backend, init.rest will contain possible flashpoint:// message
230216
// Increase memory limit in dev instance (mostly for developer page functions)
@@ -310,49 +296,47 @@ export function main(init: Init): void {
310296
// Update flashpoint:// protocol registration state
311297
setProtocolRegistrationState(state.preferences.registerProtocol);
312298
});
313-
}
314299

315-
// Open websocket to backend, for communication between front and back
316-
const ws = await timeout<WebSocket>(new Promise((resolve, reject) => {
317-
const sock = new WebSocket(state.backHost.href);
318-
sock.onclose = () => { reject(new Error('Failed to authenticate connection to back.')); };
319-
sock.onerror = (event) => { reject(event.error); };
320-
sock.onopen = () => {
321-
sock.onmessage = () => {
322-
sock.onclose = noop;
323-
sock.onerror = noop;
324-
resolve(sock);
300+
// Open websocket to backend, so we can get QUIT events
301+
const ws = await timeout<WebSocket>(new Promise((resolve, reject) => {
302+
const sock = new WebSocket(state.backHost.href);
303+
sock.onclose = () => { reject(new Error('Failed to authenticate connection to back.')); };
304+
sock.onerror = (event) => { reject(event.error); };
305+
sock.onopen = () => {
306+
sock.onmessage = () => {
307+
sock.onclose = noop;
308+
sock.onerror = noop;
309+
resolve(sock);
310+
};
311+
sock.send('flashpoint-launcher');
325312
};
326-
sock.send('flashpoint-launcher');
327-
};
328-
}), TIMEOUT_DELAY);
329-
state.socket.setSocket(ws);
330-
state.socket.killOnDisconnect = true;
313+
}), TIMEOUT_DELAY);
314+
state.socket.setSocket(ws);
315+
state.socket.killOnDisconnect = true;
316+
}
317+
318+
// Start appropriate frontend
319+
if (opts.browserFrontend) {
320+
await app.whenReady();
321+
322+
// Create headless server
323+
const hostname = init.args['browser-mode-host'] || 'localhost';
324+
const port = init.args['browser-mode-port'] || 9000;
325+
const url = init.args['browser-mode-url'] || `http://${hostname}:${port}/`;
326+
createHeadlessServer(hostname, num(port), url);
327+
}
331328

332-
// Start frontend
333-
if (opts.frontend) {
329+
if (opts.electronWindow) {
334330
await app.whenReady();
335-
// Create main window
331+
336332
if (!state.window) {
337333
state.window = createMainWindow();
338334
}
339-
} else {
340-
// Frontend not running, give Backend init message from Main
341-
state.socket.send(BackIn.INIT_LISTEN);
342335
}
343336
}
344337

345338
async function onAppReady() {
346339
// Enable DoH
347-
// app.configureHostResolver({
348-
// secureDnsMode: 'secure',
349-
// secureDnsServers: ['https://cloudflare-dns.com/dns-query', 'https://dns.google/dns-query']
350-
// });
351-
352-
if (Util.isDev) {
353-
await installExtension( REACT_DEVELOPER_TOOLS );
354-
await installExtension( REDUX_DEVTOOLS );
355-
}
356340

357341
// Send locale code (if it has no been sent already)
358342
if (process.platform === 'win32' && !state._sentLocaleCode && state.socket.client.socket) {
@@ -518,22 +502,24 @@ export function main(init: Init): void {
518502
height: height,
519503
minWidth: 200,
520504
minHeight: 200,
521-
frame: !state.preferences.useCustomTitlebar,
505+
frame: true,
506+
// frame: !state.preferences.useCustomTitlebar,
522507
icon: path.join(__dirname, '../window/images/icon.png'),
523508
webPreferences: {
524509
preload: path.resolve(__dirname, 'preload.js'),
525510
nodeIntegration: false,
526511
contextIsolation: true,
527512
},
528513
});
514+
529515
// Add protocol report func
530516
ipcMain.on(WindowIPC.PROTOCOL, () => {
531517
if (init.protocol) {
532518
window.webContents.send(WindowIPC.PROTOCOL, init.protocol);
533519
}
534520
});
535521
// Remove the menu bar
536-
window.setMenu(null);
522+
// window.setMenu(null);
537523
// and load the index.html of the app.
538524
window.loadFile(path.join(__dirname, '../window/renderer.html'));
539525
// Open the DevTools. Don't open if using a remote debugger (like vscode)

src/main/headless.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { app, Menu, shell, Tray } from 'electron';
2+
import express from 'express';
3+
import path from 'node:path';
4+
5+
export async function createHeadlessServer(hostname: string, port: number, url: string) {
6+
const expressApp = express();
7+
console.log(__dirname);
8+
9+
// Static files
10+
expressApp.use(express.static(path.join(__dirname, '../window')));
11+
12+
// React renderer
13+
expressApp.get('/{*splat}', (req, res) => {
14+
res.sendFile(path.join(__dirname, '../window/renderer.html'));
15+
});
16+
17+
// Start the HTTP server
18+
const server = expressApp.listen(port, hostname, () => {
19+
console.log(`Headless server listening on ${hostname}:${port}`);
20+
console.log(`Starting in headless mode. Opening your browser to: ${url}`);
21+
shell.openExternal(url);
22+
});
23+
24+
const icon = process.platform === 'win32' ? '../window/images/icon.ico' : '../window/images/icon.png';
25+
const tray = new Tray(path.join(__dirname, icon));
26+
const contextMenu = Menu.buildFromTemplate([
27+
{
28+
label: 'Open in Browser',
29+
click: () => {
30+
shell.openExternal(url);
31+
}
32+
},
33+
{
34+
label: 'Exit',
35+
click: () => {
36+
server.close(() => {
37+
app.quit();
38+
});
39+
// Give server 3 seconds to close
40+
setTimeout(() => app.quit(), 3000);
41+
}
42+
}
43+
]);
44+
tray.setToolTip('Flashpoint Launcher');
45+
tray.setContextMenu(contextMenu);
46+
}

0 commit comments

Comments
 (0)