mirror of
https://github.com/Pandipipas/scoreko-electron-dev.git
synced 2026-06-05 21:22:07 +00:00
202 lines
6.3 KiB
TypeScript
202 lines
6.3 KiB
TypeScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
|
|
import { getManagedNodecgRuntimePath } from "../app/paths";
|
|
|
|
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;
|
|
statSync: (filePath: string) => { isDirectory: () => boolean };
|
|
symlinkSync: (target: string, path: string, type: "junction") => unknown;
|
|
};
|
|
|
|
export type PreparedNodecgRuntime = {
|
|
runtimePath: string;
|
|
installed: boolean;
|
|
};
|
|
|
|
type RuntimeManifest = {
|
|
appVersion?: unknown;
|
|
bundleName?: unknown;
|
|
sourceRuntime?: RuntimeManifest | null;
|
|
bundleVersion?: unknown;
|
|
generatedAt?: 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): PreparedNodecgRuntime {
|
|
const resolvedDeps = resolveDeps(deps);
|
|
const targetRuntimePath = getManagedNodecgRuntimePath(userDataPath);
|
|
|
|
validateSourceRuntime(sourceRuntimePath, bundleName, resolvedDeps.existsSync);
|
|
resolvedDeps.mkdirSync(targetRuntimePath, { recursive: true });
|
|
|
|
const installed = shouldInstallRuntime(sourceRuntimePath, targetRuntimePath, appVersion, bundleName, resolvedDeps);
|
|
|
|
if (installed) {
|
|
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 { runtimePath: targetRuntimePath, installed };
|
|
}
|
|
|
|
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,
|
|
statSync: deps?.statSync ?? fs.statSync,
|
|
symlinkSync: deps?.symlinkSync ?? fs.symlinkSync,
|
|
};
|
|
}
|
|
|
|
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?.generatedAt !== sourceMarker?.generatedAt ||
|
|
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 });
|
|
}
|
|
|
|
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(
|
|
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;
|
|
}
|
|
}
|