mirror of
https://github.com/Pandipipas/scoreko-electron-dev.git
synced 2026-06-06 05:32:06 +00:00
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:
@@ -121,7 +121,7 @@ export function createNodecgProcessManager({
|
||||
exitDetails,
|
||||
stderrDetails,
|
||||
`NodeCG path: ${nodecgRootPath}`,
|
||||
"Check that lib/nodecg dependencies are installed and the bundle exists.",
|
||||
"Check that the packaged runtime was installed correctly and the bundle exists.",
|
||||
].join("\n"),
|
||||
);
|
||||
}
|
||||
@@ -234,7 +234,7 @@ function validateNodecgInstall(
|
||||
}
|
||||
|
||||
if (!pathExists(indexPath)) {
|
||||
throw new Error(`${indexPath} was not found. Copy a full NodeCG installation into lib/nodecg.`);
|
||||
throw new Error(`${indexPath} was not found. Build the packaged NodeCG runtime before starting Electron.`);
|
||||
}
|
||||
|
||||
if (!pathExists(nodecgBootstrapPath)) {
|
||||
@@ -242,8 +242,8 @@ function validateNodecgInstall(
|
||||
[
|
||||
"NodeCG is present but internal dependencies are missing.",
|
||||
`Not found: ${nodecgBootstrapPath}`,
|
||||
"Solution: enter lib/nodecg and install dependencies:",
|
||||
" npm install",
|
||||
"Solution: rebuild the packaged runtime:",
|
||||
" npm run prepare:runtime",
|
||||
].join("\n"),
|
||||
);
|
||||
}
|
||||
@@ -253,7 +253,7 @@ function validateNodecgInstall(
|
||||
[
|
||||
`Bundle '${bundleName}' was not found.`,
|
||||
`Expected path: ${bundlePath}`,
|
||||
"Copy/clone your bundle inside lib/nodecg/bundles before running Electron.",
|
||||
"Build and package the Scoreko bundle before running Electron.",
|
||||
].join("\n"),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
type RuntimeProvisionerConfig = {
|
||||
sourceRuntimePath: string;
|
||||
userDataPath: string;
|
||||
appVersion: string;
|
||||
bundleName: string;
|
||||
log: (...args: unknown[]) => void;
|
||||
deps?: Partial<RuntimeProvisionerDeps>;
|
||||
};
|
||||
|
||||
type RuntimeProvisionerDeps = {
|
||||
existsSync: (candidatePath: string) => boolean;
|
||||
mkdirSync: (candidatePath: string, options: { recursive: true }) => unknown;
|
||||
rmSync: (candidatePath: string, options: { recursive: true; force: true }) => unknown;
|
||||
cpSync: (
|
||||
sourcePath: string,
|
||||
targetPath: string,
|
||||
options: {
|
||||
recursive: true;
|
||||
force: true;
|
||||
dereference: true;
|
||||
filter: (sourcePath: string) => boolean;
|
||||
},
|
||||
) => unknown;
|
||||
readFileSync: (filePath: string) => string | Buffer;
|
||||
writeFileSync: (filePath: string, content: string) => unknown;
|
||||
};
|
||||
|
||||
type RuntimeManifest = {
|
||||
appVersion?: unknown;
|
||||
bundleName?: unknown;
|
||||
sourceRuntime?: RuntimeManifest | null;
|
||||
bundleVersion?: unknown;
|
||||
nodecgVersion?: unknown;
|
||||
};
|
||||
|
||||
const MANAGED_RUNTIME_MARKER = ".scoreko-installed-runtime.json";
|
||||
const WRITABLE_NODECG_DIRS = ["cfg", "db", "logs"] as const;
|
||||
const MANAGED_RUNTIME_ENTRIES = ["index.js", "package.json", "package-lock.json", "node_modules", "bundles"] as const;
|
||||
|
||||
export function prepareUserNodecgRuntime({
|
||||
sourceRuntimePath,
|
||||
userDataPath,
|
||||
appVersion,
|
||||
bundleName,
|
||||
log,
|
||||
deps,
|
||||
}: RuntimeProvisionerConfig): string {
|
||||
const resolvedDeps = resolveDeps(deps);
|
||||
const targetRuntimePath = path.join(userDataPath, "nodecg");
|
||||
|
||||
validateSourceRuntime(sourceRuntimePath, bundleName, resolvedDeps.existsSync);
|
||||
resolvedDeps.mkdirSync(targetRuntimePath, { recursive: true });
|
||||
|
||||
if (shouldInstallRuntime(sourceRuntimePath, targetRuntimePath, appVersion, bundleName, resolvedDeps)) {
|
||||
log(`Installing managed NodeCG runtime into ${targetRuntimePath}`);
|
||||
installManagedRuntime(sourceRuntimePath, targetRuntimePath, appVersion, bundleName, resolvedDeps);
|
||||
}
|
||||
|
||||
for (const writableDir of WRITABLE_NODECG_DIRS) {
|
||||
resolvedDeps.mkdirSync(path.join(targetRuntimePath, writableDir), { recursive: true });
|
||||
}
|
||||
|
||||
return targetRuntimePath;
|
||||
}
|
||||
|
||||
function resolveDeps(deps?: Partial<RuntimeProvisionerDeps>): RuntimeProvisionerDeps {
|
||||
return {
|
||||
existsSync: deps?.existsSync ?? fs.existsSync,
|
||||
mkdirSync: deps?.mkdirSync ?? fs.mkdirSync,
|
||||
rmSync: deps?.rmSync ?? fs.rmSync,
|
||||
cpSync: deps?.cpSync ?? fs.cpSync,
|
||||
readFileSync: deps?.readFileSync ?? fs.readFileSync,
|
||||
writeFileSync: deps?.writeFileSync ?? fs.writeFileSync,
|
||||
};
|
||||
}
|
||||
|
||||
function validateSourceRuntime(
|
||||
sourceRuntimePath: string,
|
||||
bundleName: string,
|
||||
existsSync: RuntimeProvisionerDeps["existsSync"],
|
||||
): void {
|
||||
const requiredPaths = [
|
||||
sourceRuntimePath,
|
||||
path.join(sourceRuntimePath, "index.js"),
|
||||
path.join(sourceRuntimePath, "package.json"),
|
||||
path.join(sourceRuntimePath, "node_modules", "nodecg", "dist", "server", "bootstrap.js"),
|
||||
path.join(sourceRuntimePath, "bundles", bundleName, "package.json"),
|
||||
];
|
||||
|
||||
const missingPaths = requiredPaths.filter((candidatePath) => !existsSync(candidatePath));
|
||||
|
||||
if (missingPaths.length > 0) {
|
||||
throw new Error(
|
||||
[
|
||||
"The packaged NodeCG runtime is incomplete.",
|
||||
...missingPaths.map((missingPath) => `Missing: ${missingPath}`),
|
||||
"Build the runtime with 'npm run prepare:runtime' before packaging or starting Electron.",
|
||||
].join("\n"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function shouldInstallRuntime(
|
||||
sourceRuntimePath: string,
|
||||
targetRuntimePath: string,
|
||||
appVersion: string,
|
||||
bundleName: string,
|
||||
deps: RuntimeProvisionerDeps,
|
||||
): boolean {
|
||||
const targetBootstrap = path.join(targetRuntimePath, "node_modules", "nodecg", "dist", "server", "bootstrap.js");
|
||||
const targetBundlePackage = path.join(targetRuntimePath, "bundles", bundleName, "package.json");
|
||||
|
||||
if (!deps.existsSync(targetBootstrap) || !deps.existsSync(targetBundlePackage)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const targetMarker = readJson(path.join(targetRuntimePath, MANAGED_RUNTIME_MARKER), deps);
|
||||
const sourceMarker = readJson(path.join(sourceRuntimePath, ".scoreko-runtime.json"), deps);
|
||||
|
||||
return (
|
||||
targetMarker?.appVersion !== appVersion ||
|
||||
targetMarker?.bundleName !== bundleName ||
|
||||
targetMarker?.sourceRuntime?.bundleVersion !== sourceMarker?.bundleVersion ||
|
||||
targetMarker?.sourceRuntime?.nodecgVersion !== sourceMarker?.nodecgVersion
|
||||
);
|
||||
}
|
||||
|
||||
function installManagedRuntime(
|
||||
sourceRuntimePath: string,
|
||||
targetRuntimePath: string,
|
||||
appVersion: string,
|
||||
bundleName: string,
|
||||
deps: RuntimeProvisionerDeps,
|
||||
): void {
|
||||
for (const entry of MANAGED_RUNTIME_ENTRIES) {
|
||||
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]);
|
||||
},
|
||||
});
|
||||
|
||||
const sourceRuntime = readJson(path.join(sourceRuntimePath, ".scoreko-runtime.json"), deps);
|
||||
deps.writeFileSync(
|
||||
path.join(targetRuntimePath, MANAGED_RUNTIME_MARKER),
|
||||
`${JSON.stringify(
|
||||
{
|
||||
appVersion,
|
||||
bundleName,
|
||||
sourceRuntime,
|
||||
installedAt: new Date().toISOString(),
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
}
|
||||
|
||||
function readJson(filePath: string, deps: Pick<RuntimeProvisionerDeps, "existsSync" | "readFileSync">): RuntimeManifest | null {
|
||||
if (!deps.existsSync(filePath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(String(deps.readFileSync(filePath)));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user