diff --git a/src/main/app/application-controller.ts b/src/main/app/application-controller.ts index c747592..65b8d0f 100644 --- a/src/main/app/application-controller.ts +++ b/src/main/app/application-controller.ts @@ -134,6 +134,8 @@ export function createApplicationController({ mainWindow = deps.createMainWindow(); loadingWindow = deps.createLoadingWindow(); + loadingWindow.show(); + state = "starting"; await startNodecg(); @@ -143,7 +145,6 @@ export function createApplicationController({ } await loadingWindow.loadURL(paths.loadingDashboardUrl); - loadingWindow.show(); const loadingShownAt = now(); diff --git a/src/main/nodecg/runtime-provisioner.ts b/src/main/nodecg/runtime-provisioner.ts index 631ce38..7bacc14 100644 --- a/src/main/nodecg/runtime-provisioner.ts +++ b/src/main/nodecg/runtime-provisioner.ts @@ -23,11 +23,13 @@ type RuntimeProvisionerDeps = { recursive: true; force: true; dereference: true; - filter: (sourcePath: string) => boolean; + filter?: (sourcePath: string) => boolean; }, ) => unknown; readFileSync: (filePath: string) => string | Buffer; writeFileSync: (filePath: string, content: string) => unknown; + statSync: (filePath: string) => { isDirectory: () => boolean }; + symlinkSync: (target: string, path: string, type: "junction") => unknown; }; export type PreparedNodecgRuntime = { @@ -84,6 +86,8 @@ function resolveDeps(deps?: Partial): RuntimeProvisioner cpSync: deps?.cpSync ?? fs.cpSync, readFileSync: deps?.readFileSync ?? fs.readFileSync, writeFileSync: deps?.writeFileSync ?? fs.writeFileSync, + statSync: deps?.statSync ?? fs.statSync, + symlinkSync: deps?.symlinkSync ?? fs.symlinkSync, }; } @@ -150,16 +154,20 @@ function installManagedRuntime( deps.rmSync(path.join(targetRuntimePath, entry), { recursive: true, force: true }); } - deps.cpSync(sourceRuntimePath, targetRuntimePath, { - recursive: true, - force: true, - dereference: true, - filter: (sourcePath) => { - const relativePath = path.relative(sourceRuntimePath, sourcePath); - const firstSegment = relativePath.split(path.sep)[0]; - return !WRITABLE_NODECG_DIRS.includes(firstSegment as (typeof WRITABLE_NODECG_DIRS)[number]); - }, - }); + for (const entry of MANAGED_RUNTIME_ENTRIES) { + const sourcePath = path.join(sourceRuntimePath, entry); + const targetPath = path.join(targetRuntimePath, entry); + + if (!deps.existsSync(sourcePath)) { + continue; + } + + if (deps.statSync(sourcePath).isDirectory()) { + deps.symlinkSync(sourcePath, targetPath, "junction"); + } else { + deps.cpSync(sourcePath, targetPath, { recursive: true, force: true, dereference: true }); + } + } const sourceRuntime = readJson(path.join(sourceRuntimePath, ".scoreko-runtime.json"), deps); deps.writeFileSync( diff --git a/src/tests/application-controller.test.ts b/src/tests/application-controller.test.ts index f2af329..9f6c5b9 100644 --- a/src/tests/application-controller.test.ts +++ b/src/tests/application-controller.test.ts @@ -133,10 +133,10 @@ test("ApplicationController preserves startup ordering and schedules updates aft "create-manager", "create-main", "create-loading", + "loading:show", "start-nodecg", "wait-nodecg", `loading:load:${paths.loadingDashboardUrl}`, - "loading:show", `main:load:${paths.mainDashboardUrl}`, "main:show", "loading:close", @@ -192,10 +192,10 @@ test("ApplicationController directly launches packaged app after runtime install "create-manager", "create-main", "create-loading", + "loading:show", "start-nodecg", "wait-nodecg", "loading:load:http://localhost:9090/loading", - "loading:show", "main:load:http://localhost:9090/main", "main:show", "loading:close", diff --git a/src/tests/runtime-provisioner.test.ts b/src/tests/runtime-provisioner.test.ts index 59bd04f..3675d92 100644 --- a/src/tests/runtime-provisioner.test.ts +++ b/src/tests/runtime-provisioner.test.ts @@ -38,10 +38,21 @@ function createFakeFs(initialPaths: string[] = [], initialFiles: Record { state.copied.push({ from: path.normalize(from), to: path.normalize(to) }); state.paths.add(path.normalize(to)); - state.paths.add(path.join(path.normalize(to), "index.js")); - state.paths.add(path.join(path.normalize(to), "package.json")); - state.paths.add(path.join(path.normalize(to), "node_modules", "nodecg", "dist", "server", "bootstrap.js")); - state.paths.add(path.join(path.normalize(to), "bundles", "scoreko-dev", "package.json")); + }, + statSync: (filePath: string) => ({ + isDirectory: () => { + const normalized = path.normalize(filePath); + return normalized.endsWith("node_modules") || normalized.endsWith("bundles"); + }, + }), + symlinkSync: (target: string, linkPath: string, type: string) => { + state.copied.push({ from: path.normalize(target), to: path.normalize(linkPath) }); + state.paths.add(path.normalize(linkPath)); + if (target.endsWith("node_modules")) { + state.paths.add(path.join(path.normalize(linkPath), "nodecg", "dist", "server", "bootstrap.js")); + } else if (target.endsWith("bundles")) { + state.paths.add(path.join(path.normalize(linkPath), "scoreko-dev", "package.json")); + } }, readFileSync: (filePath: string) => state.files.get(path.normalize(filePath)) ?? "{}", writeFileSync: (filePath: string, content: string) => { @@ -57,7 +68,9 @@ function getSourcePaths(source: string) { source, path.join(source, "index.js"), path.join(source, "package.json"), + path.join(source, "node_modules"), path.join(source, "node_modules", "nodecg", "dist", "server", "bootstrap.js"), + path.join(source, "bundles"), path.join(source, "bundles", "scoreko-dev", "package.json"), path.join(source, ".scoreko-runtime.json"), ]; @@ -81,7 +94,7 @@ test("prepareUserNodecgRuntime copies the packaged runtime into userData", () => assert.equal(preparedRuntime.runtimePath, path.join(userData, "nodecg")); assert.equal(preparedRuntime.installed, true); - assert.equal(state.copied.length, 1); + assert.equal(state.copied.length, 4); assert.ok(state.paths.has(path.join(userData, "nodecg", "cfg"))); assert.ok(state.paths.has(path.join(userData, "nodecg", "db"))); assert.ok(state.paths.has(path.join(userData, "nodecg", "logs"))); @@ -150,7 +163,7 @@ test("prepareUserNodecgRuntime refreshes managed files when the app version chan }); assert.equal(preparedRuntime.installed, true); - assert.equal(state.copied.length, 1); + assert.equal(state.copied.length, 4); assert.ok(state.removed.includes(path.join(target, "node_modules"))); assert.ok(state.removed.includes(path.join(target, "bundles"))); assert.ok(!state.removed.includes(path.join(target, "db"))); @@ -190,7 +203,7 @@ test("prepareUserNodecgRuntime refreshes managed files when the source runtime w }); assert.equal(preparedRuntime.installed, true); - assert.equal(state.copied.length, 1); + assert.equal(state.copied.length, 4); assert.ok(state.removed.includes(path.join(target, "bundles"))); assert.ok(!state.removed.includes(path.join(target, "cfg"))); });