mirror of
https://github.com/Pandipipas/scoreko-dev.git
synced 2026-06-06 03:32:06 +00:00
Agregar wrapper de Electron para ejecutar el bundle como app de escritorio (#36)
* Add Electron wrapper for running bundle as desktop app * Fix Electron wrapper Node runtime mismatch with NodeCG * Bump Electron wrapper to 39.5.1 and update docs * Clarify runtime behavior in troubleshooting docs * Make electron npm scripts cross-platform with cross-env * Default NodeCG launch to system Node; keep Electron-node as opt-in * Add Electron better-sqlite3 rebuild scripts for ABI issues * Update default Electron URL to dashboard standalone example
This commit is contained in:
@@ -0,0 +1,119 @@
|
|||||||
|
# Electron wrapper para Scoreko
|
||||||
|
|
||||||
|
Este wrapper permite abrir el bundle de NodeCG como una app de escritorio usando Electron.
|
||||||
|
|
||||||
|
## Requisitos
|
||||||
|
|
||||||
|
- Node.js 22+
|
||||||
|
- Electron wrapper fijado en `39.5.1` (usa Node.js `22.22.0`).
|
||||||
|
- Dependencias del proyecto raíz ya instaladas
|
||||||
|
- `node index.js` funcional en la raíz de NodeCG
|
||||||
|
|
||||||
|
## Estructura esperada
|
||||||
|
|
||||||
|
Este bundle suele vivir en `bundles/scoreko-dev`. El wrapper asume por defecto que la raíz de NodeCG está en `../../` respecto a esta carpeta.
|
||||||
|
|
||||||
|
Si tu estructura es distinta, podés usar la variable `NODECG_ROOT`.
|
||||||
|
|
||||||
|
## Instalación
|
||||||
|
|
||||||
|
Desde la raíz del repositorio:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd electron
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Uso rápido
|
||||||
|
|
||||||
|
> Nota: los scripts usan `cross-env` para que funcionen igual en Windows, macOS y Linux.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd electron
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
`npm start` arranca NodeCG con Node del sistema (`NODE_BINARY`, por defecto `node`) y después abre la ventana de Electron. Este es el modo recomendado.
|
||||||
|
|
||||||
|
Para desarrollo, usá:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run start:dev
|
||||||
|
```
|
||||||
|
|
||||||
|
`start:dev` también usa Node del sistema y deja variables preparadas para desarrollo.
|
||||||
|
|
||||||
|
## Variables de entorno
|
||||||
|
|
||||||
|
- `NODECG_ROOT`: ruta a la raíz de NodeCG (relativa al bundle o absoluta).
|
||||||
|
- `NODECG_PORT`: puerto de NodeCG (por defecto `9090`).
|
||||||
|
- `ELECTRON_START_URL`: URL que abre la ventana (por defecto `http://localhost:9090/bundles/scoreko-dev/dashboard/example/main.html?standalone=true`).
|
||||||
|
- `NODE_BINARY`: ejecutable de Node.js para arrancar NodeCG (por defecto `node`).
|
||||||
|
- `NODECG_USE_ELECTRON_NODE`: si vale `1`, NodeCG arranca con el runtime de Node embebido en Electron (`process.execPath`).
|
||||||
|
|
||||||
|
Ejemplo:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
NODECG_ROOT=../.. NODECG_PORT=9090 ELECTRON_START_URL=http://localhost:9090/bundles/scoreko-dev/dashboard/example/main.html?standalone=true npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
## Modo desarrollo recomendado
|
||||||
|
|
||||||
|
Script de desarrollo:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run start:dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Modo alternativo (experimental) si querés forzar Node embebido de Electron:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run start:electron-node
|
||||||
|
```
|
||||||
|
|
||||||
|
Si vas a usar el modo Electron-Node y te aparece `NODE_MODULE_VERSION`, recompilá `better-sqlite3` contra Electron 39.5.1:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run rebuild:better-sqlite3:electron
|
||||||
|
```
|
||||||
|
|
||||||
|
También podés hacerlo y arrancar en un paso:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run start:electron-node:rebuild
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting: `NODE_MODULE_VERSION` (better-sqlite3)
|
||||||
|
|
||||||
|
Si ves un error como:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
was compiled against a different Node.js version ...
|
||||||
|
NODE_MODULE_VERSION 127 ... requires NODE_MODULE_VERSION 136
|
||||||
|
```
|
||||||
|
|
||||||
|
significa que NodeCG se está ejecutando con un runtime distinto al que compiló los módulos nativos (`better-sqlite3`).
|
||||||
|
|
||||||
|
- `127` corresponde a Node.js 22 (normalmente tu instalación de Node).
|
||||||
|
- El otro número (`136`, `140`, etc.) representa la ABI del runtime de Node embebido en la versión de Electron que estés usando.
|
||||||
|
|
||||||
|
Recomendación práctica: usá Node del sistema (`start`/`start:dev`) para NodeCG. Si forzás `start:electron-node`, tenés que recompilar nativos (como `better-sqlite3`) contra la ABI de Electron.
|
||||||
|
|
||||||
|
Pasos recomendados en Windows:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# desde la raíz de NodeCG (no dentro de electron)
|
||||||
|
cd ~/Desktop/nodecg
|
||||||
|
rm -rf node_modules package-lock.json
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# opcional: forzar recompilación de nativos para tu Node actual
|
||||||
|
npm rebuild better-sqlite3
|
||||||
|
```
|
||||||
|
|
||||||
|
Y si tenés varias instalaciones de Node:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd bundles/scoreko-dev/electron
|
||||||
|
NODE_BINARY="C:/Program Files/nodejs/node.exe" npm start
|
||||||
|
```
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import { app, BrowserWindow, dialog } from 'electron';
|
||||||
|
import { spawn } from 'node:child_process';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { dirname, resolve } from 'node:path';
|
||||||
|
import { get } from 'node:http';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
const bundleRoot = resolve(__dirname, '..');
|
||||||
|
|
||||||
|
const nodecgRoot = process.env.NODECG_ROOT
|
||||||
|
? resolve(bundleRoot, process.env.NODECG_ROOT)
|
||||||
|
: resolve(bundleRoot, '..', '..');
|
||||||
|
|
||||||
|
const startUrl = process.env.ELECTRON_START_URL ?? 'http://localhost:9090/bundles/scoreko-dev/dashboard/example/main.html?standalone=true';
|
||||||
|
const nodecgPort = Number(process.env.NODECG_PORT ?? 9090);
|
||||||
|
const nodeBinary = process.env.NODE_BINARY ?? 'node';
|
||||||
|
const useElectronNodeForNodeCG = process.env.NODECG_USE_ELECTRON_NODE === '1';
|
||||||
|
|
||||||
|
/** @type {import('node:child_process').ChildProcess | undefined} */
|
||||||
|
let nodecgProcess;
|
||||||
|
|
||||||
|
function waitForServer(url, timeoutMs = 30_000) {
|
||||||
|
const started = Date.now();
|
||||||
|
|
||||||
|
return new Promise((resolvePromise, rejectPromise) => {
|
||||||
|
const attempt = () => {
|
||||||
|
const request = get(url, (response) => {
|
||||||
|
response.resume();
|
||||||
|
|
||||||
|
if (response.statusCode && response.statusCode >= 200 && response.statusCode < 500) {
|
||||||
|
resolvePromise();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Date.now() - started > timeoutMs) {
|
||||||
|
rejectPromise(new Error('NodeCG no respondió a tiempo.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(attempt, 500);
|
||||||
|
});
|
||||||
|
|
||||||
|
request.on('error', () => {
|
||||||
|
if (Date.now() - started > timeoutMs) {
|
||||||
|
rejectPromise(new Error('No fue posible conectar a NodeCG.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(attempt, 500);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
attempt();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function startNodeCG() {
|
||||||
|
const runtimeBinary = useElectronNodeForNodeCG ? process.execPath : nodeBinary;
|
||||||
|
|
||||||
|
nodecgProcess = spawn(runtimeBinary, ['index.js'], {
|
||||||
|
cwd: nodecgRoot,
|
||||||
|
stdio: 'inherit',
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
NODE_ENV: process.env.NODE_ENV ?? 'production',
|
||||||
|
PORT: String(nodecgPort)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
nodecgProcess.on('exit', (code) => {
|
||||||
|
if (!app.isQuiting) {
|
||||||
|
dialog.showErrorBox(
|
||||||
|
'NodeCG finalizado',
|
||||||
|
`El proceso de NodeCG terminó inesperadamente con código ${code ?? 'desconocido'}.`
|
||||||
|
);
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
nodecgProcess.on('error', (error) => {
|
||||||
|
dialog.showErrorBox(
|
||||||
|
'No se pudo iniciar NodeCG',
|
||||||
|
`No se pudo ejecutar \"${runtimeBinary}\". ${useElectronNodeForNodeCG ? 'Desactivá NODECG_USE_ELECTRON_NODE o revisá Electron.' : 'Definí NODE_BINARY con la ruta de Node.js.'}\n\nDetalle: ${error.message}`
|
||||||
|
);
|
||||||
|
app.quit();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createWindow() {
|
||||||
|
const win = new BrowserWindow({
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
|
autoHideMenuBar: true,
|
||||||
|
backgroundColor: '#000000',
|
||||||
|
webPreferences: {
|
||||||
|
contextIsolation: true,
|
||||||
|
sandbox: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await win.loadURL(startUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
app.on('before-quit', () => {
|
||||||
|
app.isQuiting = true;
|
||||||
|
|
||||||
|
if (nodecgProcess && !nodecgProcess.killed) {
|
||||||
|
nodecgProcess.kill('SIGTERM');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.whenReady().then(async () => {
|
||||||
|
startNodeCG();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await waitForServer(`http://127.0.0.1:${nodecgPort}`);
|
||||||
|
await createWindow();
|
||||||
|
} catch (error) {
|
||||||
|
dialog.showErrorBox('No se pudo iniciar', error instanceof Error ? error.message : String(error));
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on('window-all-closed', () => {
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "scoreko-dev-electron-wrapper",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "Electron wrapper para ejecutar Scoreko como app de escritorio.",
|
||||||
|
"type": "module",
|
||||||
|
"main": "main.mjs",
|
||||||
|
"scripts": {
|
||||||
|
"start": "cross-env electron .",
|
||||||
|
"start:dev": "cross-env NODECG_ROOT=../.. ELECTRON_START_URL=http://localhost:9090/bundles/scoreko-dev/dashboard/example/main.html?standalone=true electron .",
|
||||||
|
"start:electron-node": "cross-env NODECG_USE_ELECTRON_NODE=1 electron .",
|
||||||
|
"rebuild:better-sqlite3:electron": "npm --prefix ../.. rebuild better-sqlite3 --runtime=electron --target=39.5.1 --dist-url=https://electronjs.org/headers",
|
||||||
|
"start:electron-node:rebuild": "npm run rebuild:better-sqlite3:electron && npm run start:electron-node"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"electron": "39.5.1",
|
||||||
|
"cross-env": "^7.0.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user