diff --git a/docs/refactor/PHASE_1_FIX_SUMMARY.md b/docs/refactor/PHASE_1_FIX_SUMMARY.md new file mode 100644 index 0000000..b8fd7e8 --- /dev/null +++ b/docs/refactor/PHASE_1_FIX_SUMMARY.md @@ -0,0 +1,92 @@ +# Phase 1 Fix Summary + +## Scope + +This fix restores the Electron renderer after the Phase 1 architecture extraction. It keeps the new main-process structure, does not add a custom renderer, preload, or IPC layer, and does not revert the Phase 1 refactor. + +## Root Cause + +Phase 1 moved the real bootstrap module from `src/main/main.ts` to `src/main/app/bootstrap.ts`. After compilation, that changed the runtime `__dirname` from: + +```text +dist/main +``` + +to: + +```text +dist/main/app +``` + +The development root calculation still treated `__dirname` as if it were `dist/main`, so `rootPath` resolved to `dist` instead of the repository root. Electron then looked for the packaged NodeCG runtime at: + +```text +dist/lib/nodecg +``` + +instead of: + +```text +lib/nodecg +``` + +That prevented the main process from preparing and launching the correct NodeCG runtime, leaving the BrowserWindow without a valid dashboard to render. + +During verification, a second compatibility issue appeared in the packaged runtime: the Vite/NodeCG bundle imports generated files from the bundle-level `nodecg` directory, but `prepare-nodecg-runtime.mjs` did not copy that directory into `lib/nodecg/bundles/scoreko-dev`. Without it, NodeCG could start but failed to mount the `scoreko-dev` extension, and dashboard URLs returned 404. + +## Changes Made + +- `src/main/app/bootstrap.ts` + - Passes `dist/main` into the path helper by resolving one level above the compiled bootstrap directory. + - Keeps path ownership in `src/main/app/paths.ts` and preserves the extracted bootstrap architecture. + +- `scripts/prepare-nodecg-runtime.mjs` + - Copies the generated bundle `nodecg` directory into the managed NodeCG runtime. + - Treats that directory as required runtime output so an incomplete Vite/NodeCG build fails early. + +- `src/main/nodecg/runtime-provisioner.ts` + - Refreshes the managed runtime when the source runtime manifest was regenerated, even if the app and bundle versions are unchanged. + - Still preserves user-owned `cfg`, `db`, and `logs`. + +- `src/tests/runtime-provisioner.test.ts` + - Adds coverage for refreshing managed runtime files when the source runtime manifest changes. + +## Verification + +Commands run successfully: + +```text +npm run typecheck +npm test +npm run lint +``` + +Current test result: + +```text +53 tests passing +``` + +Runtime verification: + +- Rebuilt the NodeCG runtime. +- Rebuilt `better-sqlite3` for Electron 39.5.1. +- Started Electron with updates disabled and load delay set to zero. +- Confirmed NodeCG served: + - `http://127.0.0.1:9090` with `200 OK` + - `http://localhost:9090/bundles/scoreko-dev/dashboard/scoreko-dev/main.html?standalone=true` with `200 OK` + - `http://localhost:9090/bundles/scoreko-dev/dashboard/loading/main.html?standalone=true` with `200 OK` +- Confirmed the Electron renderer target loaded: + - title: `Dashboard` + - URL: `http://localhost:9090/bundles/scoreko-dev/dashboard/scoreko-dev/main.html?standalone=true#/` + - DOM element count: `311` + - visible body text included the Scoreko dashboard navigation and controls. + +## Architecture Notes + +- No preload was added. +- No IPC was added. +- No custom renderer architecture was added. +- BrowserWindow security settings remain explicit. +- NodeCG remains owned by the main process. +- Dashboard loading remains gated behind NodeCG readiness. diff --git a/scripts/prepare-nodecg-runtime.mjs b/scripts/prepare-nodecg-runtime.mjs index fee8dcb..99f4176 100644 --- a/scripts/prepare-nodecg-runtime.mjs +++ b/scripts/prepare-nodecg-runtime.mjs @@ -15,6 +15,7 @@ const bundleEntries = [ "dashboard", "extension", "graphics", + "nodecg", "schemas", "shared", "configschema.json", @@ -23,7 +24,7 @@ const bundleEntries = [ "README.md", ]; -const requiredBundleEntries = ["dashboard", "extension", "graphics", "schemas", "shared", "package.json"]; +const requiredBundleEntries = ["dashboard", "extension", "graphics", "nodecg", "schemas", "shared", "package.json"]; function readJson(filePath) { return JSON.parse(readFileSync(filePath, "utf8")); diff --git a/src/main/app/bootstrap.ts b/src/main/app/bootstrap.ts index 095b59a..5657388 100644 --- a/src/main/app/bootstrap.ts +++ b/src/main/app/bootstrap.ts @@ -1,4 +1,5 @@ import { app, BrowserWindow } from "electron"; +import path from "node:path"; import { getRuntimeConfig } from "../config/runtime-config"; import { showFatalError, log } from "../errors/error-presenter"; @@ -15,7 +16,7 @@ export function bootstrap(): void { const paths = getApplicationPaths({ appConfig, appDataPath: app.getPath("appData"), - compiledMainDir: __dirname, + compiledMainDir: path.resolve(__dirname, ".."), isDev, resourcesPath: process.resourcesPath, }); diff --git a/src/main/nodecg/runtime-provisioner.ts b/src/main/nodecg/runtime-provisioner.ts index 8a77e56..fc4eec2 100644 --- a/src/main/nodecg/runtime-provisioner.ts +++ b/src/main/nodecg/runtime-provisioner.ts @@ -38,6 +38,7 @@ type RuntimeManifest = { bundleName?: unknown; sourceRuntime?: RuntimeManifest | null; bundleVersion?: unknown; + generatedAt?: unknown; nodecgVersion?: unknown; }; @@ -131,6 +132,7 @@ function shouldInstallRuntime( targetMarker?.appVersion !== appVersion || targetMarker?.bundleName !== bundleName || targetMarker?.sourceRuntime?.bundleVersion !== sourceMarker?.bundleVersion || + targetMarker?.sourceRuntime?.generatedAt !== sourceMarker?.generatedAt || targetMarker?.sourceRuntime?.nodecgVersion !== sourceMarker?.nodecgVersion ); } diff --git a/src/tests/runtime-provisioner.test.ts b/src/tests/runtime-provisioner.test.ts index f77074d..59bd04f 100644 --- a/src/tests/runtime-provisioner.test.ts +++ b/src/tests/runtime-provisioner.test.ts @@ -92,7 +92,7 @@ test("prepareUserNodecgRuntime keeps an up-to-date runtime in place", () => { const source = path.normalize("/app/lib/nodecg"); const userData = path.normalize("/user/scoreko"); const target = path.join(userData, "nodecg"); - const sourceManifest = { bundleVersion: "0.1.0", nodecgVersion: "2.6.4" }; + const sourceManifest = { bundleVersion: "0.1.0", generatedAt: "2026-05-24T00:00:00.000Z", nodecgVersion: "2.6.4" }; const targetManifest = { appVersion: "0.1.0", bundleName: "scoreko-dev", sourceRuntime: sourceManifest }; const { state, deps } = createFakeFs( [ @@ -125,7 +125,7 @@ test("prepareUserNodecgRuntime refreshes managed files when the app version chan const source = path.normalize("/app/lib/nodecg"); const userData = path.normalize("/user/scoreko"); const target = path.join(userData, "nodecg"); - const sourceManifest = { bundleVersion: "0.1.0", nodecgVersion: "2.6.4" }; + const sourceManifest = { bundleVersion: "0.1.0", generatedAt: "2026-05-24T00:00:00.000Z", nodecgVersion: "2.6.4" }; const targetManifest = { appVersion: "0.0.9", bundleName: "scoreko-dev", sourceRuntime: sourceManifest }; const { state, deps } = createFakeFs( [ @@ -155,3 +155,42 @@ test("prepareUserNodecgRuntime refreshes managed files when the app version chan assert.ok(state.removed.includes(path.join(target, "bundles"))); assert.ok(!state.removed.includes(path.join(target, "db"))); }); + +test("prepareUserNodecgRuntime refreshes managed files when the source runtime was regenerated", () => { + const source = path.normalize("/app/lib/nodecg"); + const userData = path.normalize("/user/scoreko"); + const target = path.join(userData, "nodecg"); + const sourceManifest = { bundleVersion: "0.1.0", generatedAt: "2026-05-24T01:00:00.000Z", nodecgVersion: "2.6.4" }; + const targetSourceManifest = { + bundleVersion: "0.1.0", + generatedAt: "2026-05-24T00:00:00.000Z", + nodecgVersion: "2.6.4", + }; + const targetManifest = { appVersion: "0.1.0", bundleName: "scoreko-dev", sourceRuntime: targetSourceManifest }; + const { state, deps } = createFakeFs( + [ + ...getSourcePaths(source), + path.join(target, "node_modules", "nodecg", "dist", "server", "bootstrap.js"), + path.join(target, "bundles", "scoreko-dev", "package.json"), + path.join(target, ".scoreko-installed-runtime.json"), + ], + { + [path.join(source, ".scoreko-runtime.json")]: JSON.stringify(sourceManifest), + [path.join(target, ".scoreko-installed-runtime.json")]: JSON.stringify(targetManifest), + }, + ); + + const preparedRuntime = prepareUserNodecgRuntime({ + sourceRuntimePath: source, + userDataPath: userData, + appVersion: "0.1.0", + bundleName: "scoreko-dev", + log: () => undefined, + deps, + }); + + assert.equal(preparedRuntime.installed, true); + assert.equal(state.copied.length, 1); + assert.ok(state.removed.includes(path.join(target, "bundles"))); + assert.ok(!state.removed.includes(path.join(target, "cfg"))); +});