mirror of
https://github.com/Pandipipas/scoreko-electron-dev.git
synced 2026-06-06 05:32:06 +00:00
Remove Electron wrapper and associated scripts for Scoreko
This commit is contained in:
+1
-139
@@ -1,139 +1 @@
|
|||||||
# Logs
|
node_modules
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
|
|
||||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
|
||||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
|
||||||
|
|
||||||
# Runtime data
|
|
||||||
pids
|
|
||||||
*.pid
|
|
||||||
*.seed
|
|
||||||
*.pid.lock
|
|
||||||
|
|
||||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
|
||||||
lib-cov
|
|
||||||
|
|
||||||
# Coverage directory used by tools like istanbul
|
|
||||||
coverage
|
|
||||||
*.lcov
|
|
||||||
|
|
||||||
# nyc test coverage
|
|
||||||
.nyc_output
|
|
||||||
|
|
||||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
|
||||||
.grunt
|
|
||||||
|
|
||||||
# Bower dependency directory (https://bower.io/)
|
|
||||||
bower_components
|
|
||||||
|
|
||||||
# node-waf configuration
|
|
||||||
.lock-wscript
|
|
||||||
|
|
||||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
|
||||||
build/Release
|
|
||||||
|
|
||||||
# Dependency directories
|
|
||||||
node_modules/
|
|
||||||
jspm_packages/
|
|
||||||
|
|
||||||
# Snowpack dependency directory (https://snowpack.dev/)
|
|
||||||
web_modules/
|
|
||||||
|
|
||||||
# TypeScript cache
|
|
||||||
*.tsbuildinfo
|
|
||||||
|
|
||||||
# Optional npm cache directory
|
|
||||||
.npm
|
|
||||||
|
|
||||||
# Optional eslint cache
|
|
||||||
.eslintcache
|
|
||||||
|
|
||||||
# Optional stylelint cache
|
|
||||||
.stylelintcache
|
|
||||||
|
|
||||||
# Optional REPL history
|
|
||||||
.node_repl_history
|
|
||||||
|
|
||||||
# Output of 'npm pack'
|
|
||||||
*.tgz
|
|
||||||
|
|
||||||
# Yarn Integrity file
|
|
||||||
.yarn-integrity
|
|
||||||
|
|
||||||
# dotenv environment variable files
|
|
||||||
.env
|
|
||||||
.env.*
|
|
||||||
!.env.example
|
|
||||||
|
|
||||||
# parcel-bundler cache (https://parceljs.org/)
|
|
||||||
.cache
|
|
||||||
.parcel-cache
|
|
||||||
|
|
||||||
# Next.js build output
|
|
||||||
.next
|
|
||||||
out
|
|
||||||
|
|
||||||
# Nuxt.js build / generate output
|
|
||||||
.nuxt
|
|
||||||
dist
|
|
||||||
|
|
||||||
# Gatsby files
|
|
||||||
.cache/
|
|
||||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
|
||||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
|
||||||
# public
|
|
||||||
|
|
||||||
# vuepress build output
|
|
||||||
.vuepress/dist
|
|
||||||
|
|
||||||
# vuepress v2.x temp and cache directory
|
|
||||||
.temp
|
|
||||||
.cache
|
|
||||||
|
|
||||||
# Sveltekit cache directory
|
|
||||||
.svelte-kit/
|
|
||||||
|
|
||||||
# vitepress build output
|
|
||||||
**/.vitepress/dist
|
|
||||||
|
|
||||||
# vitepress cache directory
|
|
||||||
**/.vitepress/cache
|
|
||||||
|
|
||||||
# Docusaurus cache and generated files
|
|
||||||
.docusaurus
|
|
||||||
|
|
||||||
# Serverless directories
|
|
||||||
.serverless/
|
|
||||||
|
|
||||||
# FuseBox cache
|
|
||||||
.fusebox/
|
|
||||||
|
|
||||||
# DynamoDB Local files
|
|
||||||
.dynamodb/
|
|
||||||
|
|
||||||
# Firebase cache directory
|
|
||||||
.firebase/
|
|
||||||
|
|
||||||
# TernJS port file
|
|
||||||
.tern-port
|
|
||||||
|
|
||||||
# Stores VSCode versions used for testing VSCode extensions
|
|
||||||
.vscode-test
|
|
||||||
|
|
||||||
# yarn v3
|
|
||||||
.pnp.*
|
|
||||||
.yarn/*
|
|
||||||
!.yarn/patches
|
|
||||||
!.yarn/plugins
|
|
||||||
!.yarn/releases
|
|
||||||
!.yarn/sdks
|
|
||||||
!.yarn/versions
|
|
||||||
|
|
||||||
# Vite logs files
|
|
||||||
vite.config.js.timestamp-*
|
|
||||||
vite.config.ts.timestamp-*
|
|
||||||
|
|||||||
@@ -1,109 +0,0 @@
|
|||||||
# scoreko-electron-dev
|
|
||||||
|
|
||||||
Wrapper de Electron para empaquetar una instalación de NodeCG que incluya el bundle `scoreko-dev`, inspirado en `opeik/runback-electron` pero actualizado a Electron + TypeScript moderno.
|
|
||||||
|
|
||||||
Este wrapper está pensado para funcionar **standalone**: por defecto NodeCG corre con el **Node interno de Electron** (no requiere Node instalado en la máquina final).
|
|
||||||
|
|
||||||
## Qué hace
|
|
||||||
|
|
||||||
- Arranca `lib/nodecg/index.js` como proceso hijo desde Electron.
|
|
||||||
- Muestra una ventana de carga mientras NodeCG inicia.
|
|
||||||
- Carga el dashboard del bundle en `http://localhost:<puerto>/bundles/<bundle>/<ruta-dashboard>`.
|
|
||||||
- Empaqueta NodeCG + assets dentro de la app final con `electron-builder`.
|
|
||||||
|
|
||||||
## Estructura esperada
|
|
||||||
|
|
||||||
```text
|
|
||||||
scoreko-electron-dev/
|
|
||||||
├─ lib/
|
|
||||||
│ └─ nodecg/
|
|
||||||
│ ├─ index.js
|
|
||||||
│ ├─ node_modules/
|
|
||||||
│ └─ bundles/
|
|
||||||
│ └─ scoreko-dev/ # clonado/copiado desde tu repo scoreko-dev
|
|
||||||
├─ src/main/main.ts
|
|
||||||
├─ static/loading.html
|
|
||||||
└─ package.json
|
|
||||||
```
|
|
||||||
|
|
||||||
## Preparación con tu repo `scoreko-dev`
|
|
||||||
|
|
||||||
1. Copia o clona tu instalación de NodeCG en `lib/nodecg`.
|
|
||||||
2. Copia tu bundle `scoreko-dev` a `lib/nodecg/bundles/scoreko-dev`.
|
|
||||||
3. **Instala dependencias de NodeCG dentro de `lib/nodecg`**.
|
|
||||||
4. Instala dependencias del wrapper:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install
|
|
||||||
cd lib/nodecg
|
|
||||||
npm install
|
|
||||||
cd ../..
|
|
||||||
```
|
|
||||||
|
|
||||||
> Si no haces `npm install` dentro de `lib/nodecg`, verás errores como `Cannot find module ... node_modules/nodecg/dist/server/bootstrap.js`.
|
|
||||||
|
|
||||||
## ¿Existe Electron con `NODE_MODULE_VERSION 127`?
|
|
||||||
|
|
||||||
No en releases estables de Electron. Revisando la tabla oficial de releases de Electron, no aparece `modules=127`.
|
|
||||||
|
|
||||||
- Electron 32.x usa `NODE_MODULE_VERSION 128`
|
|
||||||
- Electron 34.x usa `NODE_MODULE_VERSION 132`
|
|
||||||
- Electron 35.x usa `NODE_MODULE_VERSION 133`
|
|
||||||
|
|
||||||
Por eso, para standalone hay que recompilar addons nativos contra la versión de Electron elegida.
|
|
||||||
|
|
||||||
## Error típico: NODE_MODULE_VERSION
|
|
||||||
|
|
||||||
Si ves un error como `better-sqlite3 ... NODE_MODULE_VERSION`, tienes módulos nativos compilados para una versión distinta de Node.
|
|
||||||
|
|
||||||
En ese caso recompila contra Electron:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run rebuild:native
|
|
||||||
```
|
|
||||||
|
|
||||||
Este script ejecuta `electron-rebuild` en `lib/nodecg` y en el workspace SQLite legacy (si existe).
|
|
||||||
|
|
||||||
Si sigues en modo standalone (default), asegúrate de no mezclar binarios compilados con otro runtime distinto al de Electron.
|
|
||||||
|
|
||||||
## Runtime de NodeCG
|
|
||||||
|
|
||||||
Por defecto (standalone):
|
|
||||||
- `NODECG_USE_SYSTEM_NODE=false`
|
|
||||||
|
|
||||||
Opcionalmente, puedes forzar Node del sistema:
|
|
||||||
- `NODECG_USE_SYSTEM_NODE=true`
|
|
||||||
- `NODECG_NODE_BINARY=node`
|
|
||||||
|
|
||||||
Ejemplo en PowerShell para usar Node del sistema (si lo necesitas):
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
$env:NODECG_USE_SYSTEM_NODE="true"
|
|
||||||
$env:NODECG_NODE_BINARY="C:\Program Files\nodejs\node.exe"
|
|
||||||
node -v
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
## Variables de entorno opcionales
|
|
||||||
|
|
||||||
Estas variables se leen en `src/main/main.ts`.
|
|
||||||
|
|
||||||
- `NODECG_PORT` (default: `9090`)
|
|
||||||
- `NODECG_BUNDLE_NAME` (default: `scoreko-dev`)
|
|
||||||
- `SCOREKO_DASHBOARD_ROUTE` (default: `dashboard/index.html`)
|
|
||||||
- `ELECTRON_LOAD_DELAY_MS` (default: `2500`)
|
|
||||||
- `NODECG_STARTUP_TIMEOUT_MS` (default: `30000`)
|
|
||||||
- `NODECG_USE_SYSTEM_NODE` (default: `false`)
|
|
||||||
- `NODECG_NODE_BINARY` (default: `node`)
|
|
||||||
|
|
||||||
## Scripts
|
|
||||||
|
|
||||||
- `npm run dev`: modo desarrollo (watch + relanzado de Electron).
|
|
||||||
- `npm run start`: build y ejecución local.
|
|
||||||
- `npm run build`: compila TypeScript y copia assets.
|
|
||||||
- `npm run pack`: genera app sin instalador.
|
|
||||||
- `npm run dist`: genera instalador/plataformas configuradas.
|
|
||||||
|
|
||||||
## Nota de seguridad
|
|
||||||
|
|
||||||
La ventana principal usa `contextIsolation: true`, `sandbox: true` y `nodeIntegration: false`. Los enlaces externos se abren en el navegador del sistema.
|
|
||||||
Generated
-5712
File diff suppressed because it is too large
Load Diff
@@ -1,73 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "scoreko-electron-dev",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"description": "Electron wrapper to run a NodeCG install with the scoreko-dev bundle",
|
|
||||||
"license": "MIT",
|
|
||||||
"private": true,
|
|
||||||
"main": "dist/main/main.js",
|
|
||||||
"scripts": {
|
|
||||||
"clean": "rimraf dist release",
|
|
||||||
"typecheck": "tsc --noEmit",
|
|
||||||
"build": "npm run clean && tsc -p tsconfig.json && node scripts/copy-assets.mjs",
|
|
||||||
"start": "npm run build && electron .",
|
|
||||||
"dev": "concurrently -k \"npm:watch\" \"npm:dev:electron\"",
|
|
||||||
"watch": "tsc -p tsconfig.json --watch",
|
|
||||||
"dev:electron": "wait-on dist/main/main.js && electron .",
|
|
||||||
"pack": "npm run build && electron-builder --dir",
|
|
||||||
"dist": "npm run build && electron-builder",
|
|
||||||
"rebuild:native": "node scripts/rebuild-nodecg-native.mjs"
|
|
||||||
},
|
|
||||||
"build": {
|
|
||||||
"appId": "com.scoreko.desktop",
|
|
||||||
"productName": "Scoreko",
|
|
||||||
"directories": {
|
|
||||||
"output": "release",
|
|
||||||
"buildResources": "static"
|
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"dist/**",
|
|
||||||
"package.json"
|
|
||||||
],
|
|
||||||
"extraResources": [
|
|
||||||
{
|
|
||||||
"from": "lib/nodecg",
|
|
||||||
"to": "lib/nodecg"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"from": "static",
|
|
||||||
"to": "static"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"mac": {
|
|
||||||
"target": [
|
|
||||||
"dmg"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"linux": {
|
|
||||||
"target": [
|
|
||||||
"AppImage"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"win": {
|
|
||||||
"target": [
|
|
||||||
"nsis"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=22"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"source-map-support": "^0.5.21"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/node": "^22.10.5",
|
|
||||||
"concurrently": "^9.1.2",
|
|
||||||
"electron": "^35.7.5",
|
|
||||||
"electron-builder": "^25.1.8",
|
|
||||||
"rimraf": "^6.0.1",
|
|
||||||
"typescript": "^5.7.3",
|
|
||||||
"wait-on": "^8.0.1",
|
|
||||||
"@electron/rebuild": "^3.7.1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { cpSync, existsSync, mkdirSync } from "node:fs";
|
|
||||||
import path from "node:path";
|
|
||||||
|
|
||||||
const root = process.cwd();
|
|
||||||
const distStatic = path.join(root, "dist", "static");
|
|
||||||
const sourceStatic = path.join(root, "static");
|
|
||||||
|
|
||||||
mkdirSync(distStatic, { recursive: true });
|
|
||||||
|
|
||||||
if (existsSync(sourceStatic)) {
|
|
||||||
cpSync(sourceStatic, distStatic, { recursive: true });
|
|
||||||
console.log("Copied static assets to dist/static");
|
|
||||||
} else {
|
|
||||||
console.warn("No static folder found, skipping copy-assets step");
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import { existsSync } from "node:fs";
|
|
||||||
import path from "node:path";
|
|
||||||
import { spawn } from "node:child_process";
|
|
||||||
|
|
||||||
const root = process.cwd();
|
|
||||||
const candidates = [
|
|
||||||
path.join(root, "lib", "nodecg"),
|
|
||||||
path.join(root, "lib", "nodecg", "workspaces", "database-adapter-sqlite-legacy"),
|
|
||||||
];
|
|
||||||
|
|
||||||
const moduleDirs = candidates.filter((dir) => existsSync(path.join(dir, "package.json")));
|
|
||||||
|
|
||||||
if (moduleDirs.length === 0) {
|
|
||||||
console.error("No NodeCG package folders found. Expected lib/nodecg and/or workspaces.");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function run(command, args, cwd) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const child = spawn(command, args, {
|
|
||||||
cwd,
|
|
||||||
stdio: "inherit",
|
|
||||||
shell: process.platform === "win32",
|
|
||||||
env: {
|
|
||||||
...process.env,
|
|
||||||
npm_config_runtime: "electron",
|
|
||||||
npm_config_build_from_source: "false",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
child.on("exit", (code) => {
|
|
||||||
if (code === 0) {
|
|
||||||
resolve();
|
|
||||||
} else {
|
|
||||||
reject(new Error(`${command} ${args.join(" ")} failed with code ${code}`));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const dir of moduleDirs) {
|
|
||||||
console.log(`\n[rebuild-native] Rebuilding native modules in: ${dir}`);
|
|
||||||
await run("npx", ["electron-rebuild", "--force", "--only", "better-sqlite3"], dir);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("\n[rebuild-native] Done.");
|
|
||||||
@@ -1,297 +0,0 @@
|
|||||||
import { app, BrowserWindow, dialog, shell } from "electron";
|
|
||||||
import { ChildProcess, spawn } from "node:child_process";
|
|
||||||
import fs from "node:fs";
|
|
||||||
import path from "node:path";
|
|
||||||
|
|
||||||
const APP_TITLE = "Scoreko";
|
|
||||||
const DEFAULT_NODECG_PORT = process.env.NODECG_PORT ?? "9090";
|
|
||||||
const DEFAULT_BUNDLE_NAME = process.env.NODECG_BUNDLE_NAME ?? "scoreko-dev";
|
|
||||||
const DEFAULT_DASHBOARD_ROUTE = process.env.SCOREKO_DASHBOARD_ROUTE ?? "dashboard/index.html";
|
|
||||||
const LOAD_DELAY_MS = Number.parseInt(process.env.ELECTRON_LOAD_DELAY_MS ?? "2500", 10);
|
|
||||||
const STARTUP_TIMEOUT_MS = Number.parseInt(process.env.NODECG_STARTUP_TIMEOUT_MS ?? "30000", 10);
|
|
||||||
const USE_SYSTEM_NODE = (process.env.NODECG_USE_SYSTEM_NODE ?? "false").toLowerCase() === "true";
|
|
||||||
const NODE_BINARY = process.env.NODECG_NODE_BINARY ?? "node";
|
|
||||||
|
|
||||||
const isDev = !app.isPackaged;
|
|
||||||
const rootPath = isDev ? path.resolve(__dirname, "../..") : process.resourcesPath;
|
|
||||||
const nodecgPath = path.resolve(rootPath, "lib", "nodecg");
|
|
||||||
const loadingPath = path.resolve(rootPath, "static", "loading.html");
|
|
||||||
|
|
||||||
const dashboardUrl = `http://localhost:${DEFAULT_NODECG_PORT}/bundles/${DEFAULT_BUNDLE_NAME}/${DEFAULT_DASHBOARD_ROUTE}`;
|
|
||||||
const baseUrl = `http://127.0.0.1:${DEFAULT_NODECG_PORT}`;
|
|
||||||
|
|
||||||
let mainWindow: BrowserWindow | null = null;
|
|
||||||
let loadingWindow: BrowserWindow | null = null;
|
|
||||||
let nodecgProcess: ChildProcess | null = null;
|
|
||||||
let lastNodeCGOutput = "";
|
|
||||||
|
|
||||||
function createMainWindow(): BrowserWindow {
|
|
||||||
const win = new BrowserWindow({
|
|
||||||
show: false,
|
|
||||||
title: APP_TITLE,
|
|
||||||
width: 1440,
|
|
||||||
height: 900,
|
|
||||||
minWidth: 960,
|
|
||||||
minHeight: 640,
|
|
||||||
backgroundColor: "#0f0f0f",
|
|
||||||
webPreferences: {
|
|
||||||
contextIsolation: true,
|
|
||||||
sandbox: true,
|
|
||||||
nodeIntegration: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
win.setMenuBarVisibility(false);
|
|
||||||
|
|
||||||
win.webContents.setWindowOpenHandler(({ url }) => {
|
|
||||||
shell.openExternal(url).catch((error) => {
|
|
||||||
log("Error opening external url", url, error);
|
|
||||||
});
|
|
||||||
|
|
||||||
return { action: "deny" };
|
|
||||||
});
|
|
||||||
|
|
||||||
win.webContents.on("will-navigate", (event, url) => {
|
|
||||||
if (url !== dashboardUrl) {
|
|
||||||
event.preventDefault();
|
|
||||||
shell.openExternal(url).catch((error) => {
|
|
||||||
log("Error opening navigation url", url, error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
win.on("page-title-updated", (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
});
|
|
||||||
|
|
||||||
return win;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createLoadingWindow(): BrowserWindow {
|
|
||||||
const win = new BrowserWindow({
|
|
||||||
show: false,
|
|
||||||
frame: false,
|
|
||||||
title: APP_TITLE,
|
|
||||||
width: 420,
|
|
||||||
height: 280,
|
|
||||||
resizable: false,
|
|
||||||
movable: true,
|
|
||||||
minimizable: false,
|
|
||||||
maximizable: false,
|
|
||||||
backgroundColor: "#0f0f0f",
|
|
||||||
webPreferences: {
|
|
||||||
contextIsolation: true,
|
|
||||||
sandbox: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
win.on("page-title-updated", (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
});
|
|
||||||
|
|
||||||
return win;
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateNodeCGInstall(): void {
|
|
||||||
const indexPath = path.join(nodecgPath, "index.js");
|
|
||||||
const nodecgBootstrapPath = path.join(nodecgPath, "node_modules", "nodecg", "dist", "server", "bootstrap.js");
|
|
||||||
const bundlePath = path.join(nodecgPath, "bundles", DEFAULT_BUNDLE_NAME);
|
|
||||||
|
|
||||||
if (!fs.existsSync(nodecgPath)) {
|
|
||||||
throw new Error(`No existe la carpeta NodeCG: ${nodecgPath}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fs.existsSync(indexPath)) {
|
|
||||||
throw new Error(`No se encontró ${indexPath}. Copia una instalación completa de NodeCG en lib/nodecg.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fs.existsSync(nodecgBootstrapPath)) {
|
|
||||||
throw new Error(
|
|
||||||
[
|
|
||||||
"NodeCG está presente pero faltan dependencias internas.",
|
|
||||||
`No existe: ${nodecgBootstrapPath}`,
|
|
||||||
"Solución: entra a lib/nodecg e instala dependencias:",
|
|
||||||
" npm install",
|
|
||||||
].join("\n"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fs.existsSync(bundlePath)) {
|
|
||||||
throw new Error(
|
|
||||||
[
|
|
||||||
`No se encontró el bundle '${DEFAULT_BUNDLE_NAME}'.`,
|
|
||||||
`Ruta esperada: ${bundlePath}`,
|
|
||||||
"Copia/clona tu bundle dentro de lib/nodecg/bundles antes de ejecutar Electron.",
|
|
||||||
].join("\n"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function enrichNodeCGFailureMessage(baseMessage: string): string {
|
|
||||||
if (lastNodeCGOutput.includes("NODE_MODULE_VERSION")) {
|
|
||||||
return [
|
|
||||||
baseMessage,
|
|
||||||
"",
|
|
||||||
"Detectado error de módulos nativos compilados para otra versión de Node (NODE_MODULE_VERSION).",
|
|
||||||
USE_SYSTEM_NODE
|
|
||||||
? "Estás en modo Node del sistema: asegúrate de lanzar con Node 22 y recompilar dependencias nativas."
|
|
||||||
: "Estás en modo standalone (Node interno de Electron). Reinstala/rebuild de dependencias con esta versión de Electron.",
|
|
||||||
"",
|
|
||||||
"Solución recomendada (en lib/nodecg):",
|
|
||||||
" 1) npm install",
|
|
||||||
" 2) npm rebuild better-sqlite3 --update-binary",
|
|
||||||
].join("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
return baseMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
function startNodeCG(): ChildProcess {
|
|
||||||
validateNodeCGInstall();
|
|
||||||
|
|
||||||
const indexPath = path.join(nodecgPath, "index.js");
|
|
||||||
const runtimeBinary = USE_SYSTEM_NODE ? NODE_BINARY : process.execPath;
|
|
||||||
const runtimeName = USE_SYSTEM_NODE ? `system node (${NODE_BINARY})` : "electron internal node";
|
|
||||||
|
|
||||||
const child = spawn(runtimeBinary, [indexPath], {
|
|
||||||
cwd: nodecgPath,
|
|
||||||
env: {
|
|
||||||
...process.env,
|
|
||||||
NODE_ENV: isDev ? "development" : "production",
|
|
||||||
NODECG_PORT: DEFAULT_NODECG_PORT,
|
|
||||||
...(USE_SYSTEM_NODE ? {} : { ELECTRON_RUN_AS_NODE: "1" }),
|
|
||||||
},
|
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
|
||||||
shell: process.platform === "win32",
|
|
||||||
});
|
|
||||||
|
|
||||||
child.stdout?.on("data", (chunk) => {
|
|
||||||
const text = String(chunk);
|
|
||||||
process.stdout.write(text);
|
|
||||||
lastNodeCGOutput = `${lastNodeCGOutput}${text}`.slice(-20000);
|
|
||||||
});
|
|
||||||
|
|
||||||
child.stderr?.on("data", (chunk) => {
|
|
||||||
const text = String(chunk);
|
|
||||||
process.stderr.write(text);
|
|
||||||
lastNodeCGOutput = `${lastNodeCGOutput}${text}`.slice(-20000);
|
|
||||||
});
|
|
||||||
|
|
||||||
log(`NodeCG started with pid=${child.pid} using ${runtimeName}`);
|
|
||||||
|
|
||||||
child.on("exit", (code, signal) => {
|
|
||||||
log(`NodeCG exited code=${code} signal=${signal ?? "none"}`);
|
|
||||||
nodecgProcess = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
return child;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function waitForNodeCGReady(startTime: number): Promise<void> {
|
|
||||||
while (Date.now() - startTime < STARTUP_TIMEOUT_MS) {
|
|
||||||
if (!nodecgProcess) {
|
|
||||||
throw new Error("NodeCG terminó antes de estar listo.");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(baseUrl, { method: "GET" });
|
|
||||||
if (response.ok || response.status === 404) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// retry until timeout
|
|
||||||
}
|
|
||||||
|
|
||||||
await sleep(500);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`Timeout esperando NodeCG en ${baseUrl} (${STARTUP_TIMEOUT_MS}ms).`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function sleep(ms: number): Promise<void> {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
setTimeout(resolve, ms);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function launch(): Promise<void> {
|
|
||||||
mainWindow = createMainWindow();
|
|
||||||
loadingWindow = createLoadingWindow();
|
|
||||||
|
|
||||||
await loadingWindow.loadFile(loadingPath);
|
|
||||||
loadingWindow.show();
|
|
||||||
|
|
||||||
lastNodeCGOutput = "";
|
|
||||||
nodecgProcess = startNodeCG();
|
|
||||||
|
|
||||||
await sleep(Math.max(0, LOAD_DELAY_MS));
|
|
||||||
await waitForNodeCGReady(Date.now());
|
|
||||||
|
|
||||||
if (!mainWindow) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await mainWindow.loadURL(dashboardUrl);
|
|
||||||
mainWindow.show();
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(`No se pudo cargar el dashboard en ${dashboardUrl}. ${String(error)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loadingWindow && !loadingWindow.isDestroyed()) {
|
|
||||||
loadingWindow.close();
|
|
||||||
loadingWindow = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopNodeCG(): void {
|
|
||||||
if (!nodecgProcess || nodecgProcess.killed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
log(`Stopping NodeCG pid=${nodecgProcess.pid}`);
|
|
||||||
nodecgProcess.kill("SIGTERM");
|
|
||||||
}
|
|
||||||
|
|
||||||
function log(...args: unknown[]): void {
|
|
||||||
console.log("[scoreko-electron]", ...args);
|
|
||||||
}
|
|
||||||
|
|
||||||
app.on("ready", () => {
|
|
||||||
launch().catch(async (error: unknown) => {
|
|
||||||
console.error("Failed to launch Scoreko wrapper", error);
|
|
||||||
|
|
||||||
const detail = enrichNodeCGFailureMessage(error instanceof Error ? error.message : String(error));
|
|
||||||
|
|
||||||
await dialog.showMessageBox({
|
|
||||||
type: "error",
|
|
||||||
title: "No se pudo iniciar Scoreko",
|
|
||||||
message: "Fallo al iniciar NodeCG",
|
|
||||||
detail,
|
|
||||||
});
|
|
||||||
|
|
||||||
app.exit(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
app.on("activate", async () => {
|
|
||||||
if (BrowserWindow.getAllWindows().length === 0) {
|
|
||||||
mainWindow = createMainWindow();
|
|
||||||
await mainWindow.loadURL(dashboardUrl);
|
|
||||||
mainWindow.show();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.on("window-all-closed", () => {
|
|
||||||
if (process.platform !== "darwin") {
|
|
||||||
app.quit();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.on("before-quit", () => {
|
|
||||||
stopNodeCG();
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on("exit", () => {
|
|
||||||
stopNodeCG();
|
|
||||||
});
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Scoreko</title>
|
|
||||||
<style>
|
|
||||||
:root {
|
|
||||||
color-scheme: dark;
|
|
||||||
font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
min-height: 100vh;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
background: #0f0f0f;
|
|
||||||
color: #d6d6d6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: 0.02em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtitle {
|
|
||||||
margin-top: 8px;
|
|
||||||
color: #9a9a9a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spinner {
|
|
||||||
margin: 24px auto 0;
|
|
||||||
width: 30px;
|
|
||||||
aspect-ratio: 1;
|
|
||||||
border: 3px solid #2f2f2f;
|
|
||||||
border-top-color: #3b82f6;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 0.9s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="card">
|
|
||||||
<div class="title">Scoreko</div>
|
|
||||||
<div class="subtitle">Inicializando NodeCG y dashboard…</div>
|
|
||||||
<div class="spinner" aria-hidden="true"></div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ES2022",
|
|
||||||
"module": "CommonJS",
|
|
||||||
"moduleResolution": "Node",
|
|
||||||
"outDir": "dist",
|
|
||||||
"rootDir": "src",
|
|
||||||
"strict": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"sourceMap": true,
|
|
||||||
"types": ["node"]
|
|
||||||
},
|
|
||||||
"include": ["src/**/*.ts"]
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user