feat(electron): build compressed Windows installer with Electron 40.6.1

This commit is contained in:
Pandipipas
2026-03-05 23:41:56 +01:00
parent 20cc81e696
commit d956dfb30b
4 changed files with 239 additions and 1 deletions
+49
View File
@@ -0,0 +1,49 @@
# Electron wrapper (Windows)
Este wrapper crea una app de escritorio para Windows que lanza NodeCG sin requerir que el usuario final tenga Node.js instalado.
## Requisitos de build (solo para quien genera el instalador)
1. Instalar dependencias del bundle raíz:
```bash
pnpm install
```
2. Instalar dependencias del wrapper:
```bash
cd electron
pnpm install
```
## Desarrollo local
Desde `electron/`:
```bash
pnpm start
```
## Generar instalador `.exe` (comprimido)
Desde `electron/`:
```bash
pnpm dist:win
```
Esto genera un instalador NSIS en `electron/dist/` con compresión máxima (`compression: maximum`).
## Qué incluye el instalador
- Runtime de Electron (incluye Node embebido).
- Dependencia `nodecg` dentro de la app.
- El bundle `scoreko-dev` como recurso (`resources/bundle`).
Con eso, el usuario final instala y ejecuta la app sin instalar Node.js aparte.
## Variables opcionales
- `NODECG_PORT` (por defecto `9090`)
- `NODECG_HOST` (por defecto `127.0.0.1`)
+118
View File
@@ -0,0 +1,118 @@
import { app, BrowserWindow } from 'electron';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { spawn } from 'node:child_process';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const BUNDLE_ROOT = app.isPackaged
? path.join(process.resourcesPath, 'bundle')
: path.resolve(__dirname, '..');
const NODECG_PORT = Number.parseInt(process.env.NODECG_PORT ?? '9090', 10);
const NODECG_HOST = process.env.NODECG_HOST ?? '127.0.0.1';
const NODECG_URL = `http://${NODECG_HOST}:${NODECG_PORT}`;
let nodecgProcess;
function getNodecgCliPath() {
return app.isPackaged
? path.join(process.resourcesPath, 'app.asar.unpacked', 'node_modules', 'nodecg', 'bin', 'nodecg.js')
: path.join(__dirname, 'node_modules', 'nodecg', 'bin', 'nodecg.js');
}
function startNodecg() {
const nodecgCli = getNodecgCliPath();
nodecgProcess = spawn(process.execPath, [nodecgCli, 'start'], {
cwd: BUNDLE_ROOT,
env: {
...process.env,
ELECTRON_RUN_AS_NODE: '1',
NODECG_PORT: String(NODECG_PORT),
},
stdio: 'inherit',
});
nodecgProcess.on('exit', (code) => {
if (code !== 0) {
console.error(`[electron] NodeCG process exited with code ${code}`);
}
if (!app.isQuitting) {
app.quit();
}
});
}
function stopNodecg() {
if (!nodecgProcess || nodecgProcess.killed) {
return;
}
app.isQuitting = true;
nodecgProcess.kill();
}
async function waitForNodecg(retries = 80, delayMs = 250) {
for (let attempt = 0; attempt < retries; attempt += 1) {
try {
const response = await fetch(NODECG_URL, { method: 'HEAD' });
if (response.ok || response.status >= 300) {
return;
}
} catch {
// Ignore connection errors while NodeCG boots.
}
await new Promise((resolve) => {
setTimeout(resolve, delayMs);
});
}
throw new Error(`NodeCG did not start in time at ${NODECG_URL}`);
}
async function createWindow() {
const mainWindow = new BrowserWindow({
width: 1366,
height: 768,
autoHideMenuBar: true,
webPreferences: {
contextIsolation: true,
sandbox: true,
},
});
await mainWindow.loadURL(NODECG_URL);
}
app.whenReady().then(async () => {
startNodecg();
try {
await waitForNodecg();
await createWindow();
} catch (error) {
console.error('[electron] Failed to start wrapper:', error);
app.quit();
}
app.on('activate', async () => {
if (BrowserWindow.getAllWindows().length === 0) {
await createWindow();
}
});
});
app.on('before-quit', () => {
app.isQuitting = true;
stopNodecg();
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
+69
View File
@@ -0,0 +1,69 @@
{
"name": "scoreko-dev-electron-wrapper",
"version": "0.2.0",
"private": true,
"description": "Electron wrapper for running the scoreko-dev NodeCG bundle on Windows.",
"main": "main.mjs",
"type": "module",
"scripts": {
"start": "electron .",
"dist:win": "electron-builder --win nsis"
},
"dependencies": {
"electron": "40.6.1",
"nodecg": "^2.6.4"
},
"devDependencies": {
"electron-builder": "^26.0.12"
},
"build": {
"appId": "com.scoreko.dev",
"productName": "Scoreko Dev",
"compression": "maximum",
"asar": true,
"directories": {
"output": "dist"
},
"files": [
"main.mjs",
"package.json",
"node_modules/**/*"
],
"extraResources": [
{
"from": "../",
"to": "bundle",
"filter": [
"package.json",
"configschema.json",
"LICENSE",
"README.md",
"schemas/**/*",
"dashboard/**/*",
"graphics/**/*",
"extension/**/*",
"src/**/*",
"node_modules/**/*",
"!electron/**/*",
"!.git/**/*"
]
}
],
"win": {
"target": [
{
"target": "nsis",
"arch": [
"x64"
]
}
]
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true,
"createDesktopShortcut": true,
"createStartMenuShortcut": true
}
}
}