feat: enhance NodeCG runtime management and packaging

- Update .gitignore and .prettierignore to exclude additional cache and configuration files.
- Revise README.md for clarity on build processes and runtime behavior.
- Improve architecture documentation to reflect changes in startup flow and module responsibilities.
- Modify troubleshooting guide to address common runtime issues and installation steps.
- Enhance ESLint configuration to ignore more directories.
- Update package.json scripts for better build and distribution processes.
- Introduce build-scoreko-bundle.mjs for building the Scoreko bundle.
- Implement prepare-nodecg-runtime.mjs for managing NodeCG runtime installation and updates.
- Add runtime-provisioner.ts to handle user-specific NodeCG runtime provisioning.
- Create tests for runtime provisioning to ensure correct behavior.
- Refactor process-manager.ts and main.ts to integrate new runtime management logic.
This commit is contained in:
2026-05-09 17:45:36 +02:00
parent b10b8adb98
commit 41e4e91c4b
16 changed files with 737 additions and 100 deletions
+154
View File
@@ -0,0 +1,154 @@
import assert from "node:assert/strict";
import path from "node:path";
import test from "node:test";
import { prepareUserNodecgRuntime } from "../main/nodecg/runtime-provisioner";
type FakeFsState = {
paths: Set<string>;
files: Map<string, string>;
removed: string[];
copied: Array<{ from: string; to: string }>;
};
function createFakeFs(initialPaths: string[] = [], initialFiles: Record<string, string> = {}) {
const state: FakeFsState = {
paths: new Set(initialPaths.map((candidatePath) => path.normalize(candidatePath))),
files: new Map(Object.entries(initialFiles).map(([filePath, content]) => [path.normalize(filePath), content])),
removed: [],
copied: [],
};
for (const filePath of state.files.keys()) {
state.paths.add(filePath);
}
return {
state,
deps: {
existsSync: (candidatePath: string) => state.paths.has(path.normalize(candidatePath)),
mkdirSync: (candidatePath: string) => {
state.paths.add(path.normalize(candidatePath));
return undefined;
},
rmSync: (candidatePath: string) => {
state.removed.push(path.normalize(candidatePath));
state.paths.delete(path.normalize(candidatePath));
},
cpSync: (from: string, to: string) => {
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"));
},
readFileSync: (filePath: string) => state.files.get(path.normalize(filePath)) ?? "{}",
writeFileSync: (filePath: string, content: string) => {
state.files.set(path.normalize(filePath), content);
state.paths.add(path.normalize(filePath));
},
},
};
}
function getSourcePaths(source: string) {
return [
source,
path.join(source, "index.js"),
path.join(source, "package.json"),
path.join(source, "node_modules", "nodecg", "dist", "server", "bootstrap.js"),
path.join(source, "bundles", "scoreko-dev", "package.json"),
path.join(source, ".scoreko-runtime.json"),
];
}
test("prepareUserNodecgRuntime copies the packaged runtime into userData", () => {
const source = path.normalize("/app/lib/nodecg");
const userData = path.normalize("/user/scoreko");
const { state, deps } = createFakeFs(getSourcePaths(source), {
[path.join(source, ".scoreko-runtime.json")]: JSON.stringify({ bundleVersion: "0.1.0", nodecgVersion: "2.6.4" }),
});
const runtimePath = prepareUserNodecgRuntime({
sourceRuntimePath: source,
userDataPath: userData,
appVersion: "0.1.0",
bundleName: "scoreko-dev",
log: () => undefined,
deps,
});
assert.equal(runtimePath, path.join(userData, "nodecg"));
assert.equal(state.copied.length, 1);
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")));
assert.ok(state.files.has(path.join(userData, "nodecg", ".scoreko-installed-runtime.json")));
});
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 targetManifest = { appVersion: "0.1.0", bundleName: "scoreko-dev", sourceRuntime: sourceManifest };
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),
},
);
prepareUserNodecgRuntime({
sourceRuntimePath: source,
userDataPath: userData,
appVersion: "0.1.0",
bundleName: "scoreko-dev",
log: () => undefined,
deps,
});
assert.equal(state.copied.length, 0);
assert.equal(state.removed.length, 0);
});
test("prepareUserNodecgRuntime refreshes managed files when the app version changes", () => {
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 targetManifest = { appVersion: "0.0.9", bundleName: "scoreko-dev", sourceRuntime: sourceManifest };
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),
},
);
prepareUserNodecgRuntime({
sourceRuntimePath: source,
userDataPath: userData,
appVersion: "0.1.0",
bundleName: "scoreko-dev",
log: () => undefined,
deps,
});
assert.equal(state.copied.length, 1);
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")));
});