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
+183
View File
@@ -0,0 +1,183 @@
#!/usr/bin/env node
import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import path from "node:path";
import { spawnSync } from "node:child_process";
const electronRoot = process.cwd();
const bundleRoot = path.resolve(electronRoot, "..");
const runtimeRoot = path.join(electronRoot, "lib", "nodecg");
const runtimeNodeModules = path.join(runtimeRoot, "node_modules");
const bundleName = process.env.NODECG_BUNDLE_NAME?.trim() || "scoreko-dev";
const runtimeBundleRoot = path.join(runtimeRoot, "bundles", bundleName);
const bundleEntries = [
"assets",
"dashboard",
"extension",
"graphics",
"schemas",
"shared",
"configschema.json",
"LICENSE",
"package.json",
"README.md",
];
const requiredBundleEntries = ["dashboard", "extension", "graphics", "schemas", "shared", "package.json"];
function readJson(filePath) {
return JSON.parse(readFileSync(filePath, "utf8"));
}
function copyIfExists(source, destination) {
if (!existsSync(source)) {
return false;
}
cpSync(source, destination, {
recursive: true,
force: true,
dereference: true,
filter: (sourcePath) => !sourcePath.split(path.sep).includes("node_modules"),
});
return true;
}
function run(command, args, cwd) {
const result = spawnSync(command, args, {
cwd,
stdio: "inherit",
shell: process.platform === "win32",
env: {
...process.env,
npm_config_cache: process.env.npm_config_cache ?? path.join(electronRoot, ".npm-runtime-cache"),
},
});
if (result.error) {
throw new Error(`${command} ${args.join(" ")} failed: ${result.error.message}`);
}
if (result.status !== 0) {
throw new Error(`${command} ${args.join(" ")} failed with code ${result.status}`);
}
}
function getInstalledNodecgVersion() {
const nodecgPackagePath = path.join(bundleRoot, "node_modules", "nodecg", "package.json");
if (!existsSync(nodecgPackagePath)) {
throw new Error(
[
"NodeCG is not installed in the parent project.",
`Expected: ${nodecgPackagePath}`,
`Run 'pnpm install' from ${bundleRoot} before packaging.`,
].join("\n"),
);
}
return readJson(nodecgPackagePath).version;
}
function assertBundleBuildExists() {
for (const entry of requiredBundleEntries) {
const source = path.join(bundleRoot, entry);
if (!existsSync(source)) {
throw new Error(
[
`The built Scoreko bundle is missing '${entry}'.`,
`Expected: ${source}`,
`Run 'pnpm build' from ${bundleRoot} before packaging.`,
].join("\n"),
);
}
}
}
function createRuntimePackageJson() {
const bundlePackageJson = readJson(path.join(bundleRoot, "package.json"));
const dependencies = {
nodecg: getInstalledNodecgVersion(),
...(bundlePackageJson.dependencies ?? {}),
};
writeFileSync(
path.join(runtimeRoot, "package.json"),
`${JSON.stringify(
{
private: true,
name: "scoreko-nodecg-runtime",
version: bundlePackageJson.version ?? "0.0.0",
description: "Packaged NodeCG runtime for Scoreko Desktop.",
type: "commonjs",
scripts: {
start: "node index.js",
},
dependencies,
},
null,
2,
)}\n`,
);
writeFileSync(path.join(runtimeRoot, "index.js"), 'require("nodecg");\n');
}
function copyBundle() {
mkdirSync(runtimeBundleRoot, { recursive: true });
for (const entry of bundleEntries) {
copyIfExists(path.join(bundleRoot, entry), path.join(runtimeBundleRoot, entry));
}
}
function writeManifest() {
const bundlePackageJson = readJson(path.join(bundleRoot, "package.json"));
const runtimePackageJson = readJson(path.join(runtimeRoot, "package.json"));
writeFileSync(
path.join(runtimeRoot, ".scoreko-runtime.json"),
`${JSON.stringify(
{
bundleName,
bundleVersion: bundlePackageJson.version ?? "0.0.0",
nodecgVersion: runtimePackageJson.dependencies.nodecg,
generatedAt: new Date().toISOString(),
},
null,
2,
)}\n`,
);
}
function installRuntimeDependencies() {
if (process.env.SCOREKO_SKIP_RUNTIME_NPM_INSTALL === "1") {
console.log("[prepare-runtime] Skipping runtime npm install by environment request.");
return;
}
const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm";
run(npmCommand, ["install", "--omit=dev", "--no-audit", "--no-fund"], runtimeRoot);
}
function main() {
assertBundleBuildExists();
rmSync(runtimeRoot, { recursive: true, force: true });
mkdirSync(runtimeNodeModules, { recursive: true });
mkdirSync(path.join(runtimeRoot, "bundles"), { recursive: true });
createRuntimePackageJson();
copyBundle();
installRuntimeDependencies();
writeManifest();
console.log(`[prepare-runtime] NodeCG runtime ready at ${runtimeRoot}`);
}
try {
main();
} catch (error) {
console.error(error instanceof Error ? error.message : error);
process.exit(1);
}