From 94810527492fc1b3e3ebb209e9cd26b80061fa2c Mon Sep 17 00:00:00 2001 From: Pandipipas <62224708+Pandipipas@users.noreply.github.com> Date: Tue, 3 Mar 2026 00:07:12 +0100 Subject: [PATCH] Add NodeCG preflight step and sanity script --- README.md | 1 + package.json | 1 + src/main/main.ts | 2 ++ src/main/nodecg/process-manager.ts | 8 +++++++- src/tests/process-manager.test.ts | 26 ++++++++++++++++++++++++++ 5 files changed, 37 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 796d7f3..130a4a7 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ Desktop app (Electron + TypeScript) to run and package `scoreko-dev` (Electron h ### Quality and diagnostics - `npm run test`: build and tests (`node:test`). +- `npm run sanity`: runs `typecheck`, `lint`, and `test` as a quick pre-release gate. - `npm run doctor`: environment/configuration diagnostics. - `npm run lint`: lint with ESLint. - `npm run lint:fix`: lint with auto-fix. diff --git a/package.json b/package.json index fec546f..0f00ebc 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "rebuild:native": "node scripts/rebuild-nodecg-native.mjs", "rebuild:better-sqlite3": "electron-rebuild --version 40.6.1 --module-dir lib/scoreko-dev/workspaces/database-adapter-sqlite-legacy --only better-sqlite3 -f", "test": "npm run build && node --test dist/tests/**/*.test.js", + "sanity": "npm run typecheck && npm run lint && npm run test", "doctor": "node scripts/doctor.mjs", "lint": "eslint . --ext .ts,.js,.mjs", "lint:fix": "npm run lint -- --fix", diff --git a/src/main/main.ts b/src/main/main.ts index e6e8b5a..ac78cf3 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -56,6 +56,8 @@ function focusExistingWindow(): void { } async function launchApplication(): Promise { + await nodecgManager.runPreflightChecks(); + // We create both windows early so startup feels instant while NodeCG is booting in the background. mainWindow = createMainWindow({ appConfig, rootPath, mainDashboardUrl }); loadingWindow = createLoadingWindow({ appConfig, rootPath }); diff --git a/src/main/nodecg/process-manager.ts b/src/main/nodecg/process-manager.ts index 0c00f5c..77d29e0 100644 --- a/src/main/nodecg/process-manager.ts +++ b/src/main/nodecg/process-manager.ts @@ -30,6 +30,7 @@ type NodecgProcessManagerDeps = { }; export type NodecgProcessManager = { + runPreflightChecks: () => Promise; startNodecgProcess: () => Promise; waitForNodecgReady: (startTime: number) => Promise; stopNodecgProcessGracefully: () => Promise; @@ -51,7 +52,7 @@ export function createNodecgProcessManager({ let lastExit: { code: number | null; signal: NodeJS.Signals | null } | null = null; let lastStderrLine: string | null = null; - const startNodecgProcess = async (): Promise => { + const runPreflightChecks = async (): Promise => { validateNodecgInstall( nodecgRootPath, resolvedDeps.platform, @@ -66,6 +67,10 @@ export function createNodecgProcessManager({ `Port ${appConfig.nodecgPort} is already in use. Stop the process using it or set NODECG_PORT before starting.`, ); } + }; + + const startNodecgProcess = async (): Promise => { + await runPreflightChecks(); const command = resolvedDeps.platform === "win32" ? "npx.cmd" : "npx"; const child = resolvedDeps.spawnProcess(command, ["nodecg", "start"], { @@ -186,6 +191,7 @@ export function createNodecgProcessManager({ }; return { + runPreflightChecks, startNodecgProcess, waitForNodecgReady, stopNodecgProcessGracefully, diff --git a/src/tests/process-manager.test.ts b/src/tests/process-manager.test.ts index c40d226..23f5a98 100644 --- a/src/tests/process-manager.test.ts +++ b/src/tests/process-manager.test.ts @@ -70,6 +70,32 @@ test("startNodeCG fails when there are no read/write permissions", async () => { }, /No read\/write permissions on scoreko app folder/); }); +test("runPreflightChecks validates install and port availability without starting NodeCG", async () => { + let spawnCalls = 0; + + const manager = createNodecgProcessManager({ + isDev: true, + nodecgRootPath: "/fake/scoreko-dev", + nodecgBaseUrl: "http://127.0.0.1:9090", + appConfig: getBaseConfig(), + log: () => undefined, + deps: { + pathExists: () => true, + hasReadWriteAccess: () => true, + probePortAvailable: async () => true, + spawnProcess: () => { + spawnCalls += 1; + throw new Error("should not spawn during preflight"); + }, + }, + }); + + await assert.doesNotReject(async () => { + await manager.runPreflightChecks(); + }); + + assert.equal(spawnCalls, 0); +}); test("waitForNodeCGReady resolves when endpoint returns 404", async () => { const child = new MockChildProcess(4321); const manager = createNodecgProcessManager({