mirror of
https://github.com/Pandipipas/scoreko-electron-dev.git
synced 2026-06-05 21:22:07 +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:
+6
-1
@@ -1,4 +1,9 @@
|
||||
node_modules
|
||||
dist
|
||||
release
|
||||
lib
|
||||
lib
|
||||
.corepack
|
||||
.electron-cache
|
||||
.localappdata
|
||||
.npm-cache
|
||||
.npm-runtime-cache
|
||||
|
||||
@@ -3,3 +3,8 @@ release
|
||||
lib/nodecg
|
||||
node_modules
|
||||
package-lock.json
|
||||
.corepack
|
||||
.electron-cache
|
||||
.localappdata
|
||||
.npm-cache
|
||||
.npm-runtime-cache
|
||||
|
||||
@@ -1,53 +1,51 @@
|
||||
# scoreko-electron
|
||||
|
||||
Desktop app (Electron + TypeScript) to run and package a NodeCG installation with the `scoreko-dev` bundle.
|
||||
Windows desktop installer for Scoreko. The packaged app includes Electron, NodeCG, the compiled `scoreko-dev` bundle, and the production modules needed to run it, so end users do not need Node.js, pnpm, or a cloned repository.
|
||||
|
||||
## Requirements
|
||||
## Build on a development machine
|
||||
|
||||
- Node.js `>=22`
|
||||
- Dependencies installed with `npm install`
|
||||
From the repository root:
|
||||
|
||||
## Available scripts
|
||||
```powershell
|
||||
pnpm install
|
||||
```
|
||||
|
||||
### Development
|
||||
Then from `scoreko-electron-dev`:
|
||||
|
||||
- `npm run dev`: compiles in watch mode and opens Electron.
|
||||
- `npm run watch`: TypeScript watch mode.
|
||||
- `npm run dev:electron`: opens Electron when `dist/main/main.js` is ready.
|
||||
- `npm run start`: full build and local run.
|
||||
```powershell
|
||||
npm install
|
||||
npm run dist:win
|
||||
```
|
||||
|
||||
### Build and distribution
|
||||
The installer is written to `scoreko-electron-dev/release/Scoreko-setup-0.1.0.exe`.
|
||||
|
||||
- `npm run clean`: removes `dist` and `release`.
|
||||
- `npm run typecheck`: validates types without emitting files.
|
||||
- `npm run build`: compiles TypeScript and copies assets.
|
||||
- `npm run pack`: generates the app without an installer (`electron-builder --dir`).
|
||||
- `npm run dist:win`: builds a Windows installer.
|
||||
- `npm run dist:linux`: builds a Linux AppImage.
|
||||
- `npm run dist:mac`: builds a macOS package.
|
||||
- `npm run dist:all`: builds artifacts for Windows, Linux, and macOS.
|
||||
## What the build does
|
||||
|
||||
### Quality and diagnostics
|
||||
- Builds the parent `scoreko-dev` bundle with `pnpm build`.
|
||||
- Creates `scoreko-electron-dev/lib/nodecg` with a small NodeCG runtime.
|
||||
- Installs production runtime modules into that runtime.
|
||||
- Rebuilds `better-sqlite3` for Electron before creating the installer.
|
||||
- Packages the runtime as an Electron extra resource outside the app archive.
|
||||
|
||||
- `npm run test`: build and tests (`node:test`).
|
||||
- `npm run doctor`: environment/configuration diagnostics.
|
||||
- `npm run lint`: lint with ESLint.
|
||||
- `npm run lint:fix`: lint with auto-fix.
|
||||
- `npm run format`: checks formatting with Prettier.
|
||||
- `npm run format:write`: applies formatting with Prettier.
|
||||
## Runtime behavior
|
||||
|
||||
### Native modules
|
||||
On first launch, Scoreko copies the packaged NodeCG runtime to the user's app data folder and runs it from there. This keeps `cfg`, `db`, and `logs` writable on Windows even when the app is installed under `Program Files`.
|
||||
|
||||
- `npm run rebuild:native`: rebuilds NodeCG native modules.
|
||||
- `npm run rebuild:better-sqlite3`: rebuilds only `better-sqlite3` for Electron.
|
||||
## Useful scripts
|
||||
|
||||
## Quick setup
|
||||
- `npm run start`: build everything and run Electron locally.
|
||||
- `npm run prepare:runtime`: recreate `lib/nodecg` from the parent bundle.
|
||||
- `npm run rebuild:native`: rebuild NodeCG native modules for Electron.
|
||||
- `npm run dist:win`: create the Windows installer.
|
||||
- `npm run doctor`: check the prepared runtime and the configured port.
|
||||
|
||||
1. Copy `.env.example` to `.env`.
|
||||
2. Adjust variables for your environment.
|
||||
3. Run `npm run doctor` before developing or packaging.
|
||||
## Configuration
|
||||
|
||||
## References
|
||||
The defaults match the parent bundle:
|
||||
|
||||
- Troubleshooting: `docs/troubleshooting.md`
|
||||
- Architecture: `docs/architecture.md`
|
||||
- `NODECG_BUNDLE_NAME=scoreko-dev`
|
||||
- `NODECG_PORT=9090`
|
||||
- `SCOREKO_DASHBOARD_ROUTE=dashboard/scoreko-dev/main.html?standalone=true`
|
||||
- `SCOREKO_LOADING_ROUTE=dashboard/loading/main.html?standalone=true`
|
||||
|
||||
Copy `.env.example` only if you need local overrides while developing.
|
||||
|
||||
@@ -3,14 +3,16 @@
|
||||
## Startup flow
|
||||
|
||||
1. `src/main/main.ts` loads `appConfig` from `config/runtime-config.ts`.
|
||||
2. Creates windows (`windows/window-factory.ts`).
|
||||
3. Starts NodeCG with `nodecg/process-manager.ts`.
|
||||
4. Waits for HTTP readiness and shows loading -> main dashboard.
|
||||
5. On shutdown, runs a single graceful-stop flow to avoid orphan processes.
|
||||
2. Copies the packaged NodeCG runtime from app resources to user data when needed (`nodecg/runtime-provisioner.ts`).
|
||||
3. Creates windows (`windows/window-factory.ts`).
|
||||
4. Starts NodeCG with `nodecg/process-manager.ts`.
|
||||
5. Waits for HTTP readiness and shows loading -> main dashboard.
|
||||
6. On shutdown, runs a single graceful-stop flow to avoid orphan processes.
|
||||
|
||||
## Main modules
|
||||
|
||||
- `config/runtime-config.ts`: read/validate env vars.
|
||||
- `nodecg/runtime-provisioner.ts`: install/refresh the managed runtime in the writable user data folder.
|
||||
- `nodecg/process-manager.ts`: start, readiness, and stop for NodeCG; install/permission/port validation.
|
||||
- `windows/window-factory.ts`: window creation and navigation policy.
|
||||
- `windows/navigation-security.ts`: internal navigation allowlist and safe external schemes.
|
||||
|
||||
+12
-7
@@ -1,14 +1,19 @@
|
||||
# Troubleshooting
|
||||
|
||||
## `NodeCG folder does not exist`
|
||||
## `The packaged NodeCG runtime is incomplete`
|
||||
|
||||
- Verify `lib/nodecg` exists.
|
||||
- Make sure the project contains a full NodeCG installation.
|
||||
- Run `npm run prepare:runtime` from `scoreko-electron-dev`.
|
||||
- If the parent bundle is not installed yet, run `pnpm install` from the repository root first.
|
||||
|
||||
## `NodeCG is present but internal dependencies are missing`
|
||||
|
||||
- Recreate the runtime with `npm run prepare:runtime`.
|
||||
- If native SQLite errors appear during launch, run `npm run rebuild:native` before packaging.
|
||||
|
||||
## `No read/write permissions on NodeCG`
|
||||
|
||||
- Adjust permissions on `lib/nodecg` for the user running Electron.
|
||||
- On Linux/macOS: `chmod -R u+rw lib/nodecg` (according to your local policy).
|
||||
- Installed builds run NodeCG from the user's app data folder, so this usually means the local development copy is locked.
|
||||
- Close any running Scoreko/NodeCG process and run `npm run start` again.
|
||||
|
||||
## `Port <PORT> is already in use`
|
||||
|
||||
@@ -17,9 +22,9 @@
|
||||
|
||||
## `Timeout while waiting for NodeCG`
|
||||
|
||||
- Check NodeCG logs in standard output.
|
||||
- Check the Electron/NodeCG output in the terminal.
|
||||
- Increase `NODECG_STARTUP_TIMEOUT_MS` if the environment is slow.
|
||||
- Verify NodeCG dependencies (`cd lib/nodecg && npm install`).
|
||||
- Recreate the runtime with `npm run prepare:runtime` if the bundle changed.
|
||||
|
||||
## macOS build fails because of icon
|
||||
|
||||
|
||||
+11
-1
@@ -3,7 +3,17 @@ import tsParser from "@typescript-eslint/parser";
|
||||
|
||||
export default [
|
||||
{
|
||||
ignores: ["dist/**", "release/**", "lib/**"],
|
||||
ignores: [
|
||||
"dist/**",
|
||||
"release/**",
|
||||
"lib/**",
|
||||
"node_modules/**",
|
||||
".corepack/**",
|
||||
".electron-cache/**",
|
||||
".localappdata/**",
|
||||
".npm-cache/**",
|
||||
".npm-runtime-cache/**",
|
||||
],
|
||||
},
|
||||
{
|
||||
files: ["**/*.ts"],
|
||||
|
||||
+12
-8
@@ -10,9 +10,12 @@
|
||||
"private": true,
|
||||
"main": "dist/main/main.js",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist release",
|
||||
"clean": "rimraf dist release lib",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"build": "npm run clean && tsc -p tsconfig.json && node scripts/copy-assets.mjs",
|
||||
"build:bundle": "node scripts/build-scoreko-bundle.mjs",
|
||||
"build:main": "tsc -p tsconfig.json && node scripts/copy-assets.mjs",
|
||||
"prepare:runtime": "node scripts/prepare-nodecg-runtime.mjs",
|
||||
"build": "npm run clean && npm run build:bundle && npm run build:main && npm run prepare:runtime",
|
||||
"start": "npm run build && electron .",
|
||||
"dev": "concurrently -k \"npm:watch\" \"npm:dev:electron\"",
|
||||
"watch": "tsc -p tsconfig.json --watch",
|
||||
@@ -20,16 +23,16 @@
|
||||
"pack": "npm run build && electron-builder --dir",
|
||||
"rebuild:native": "node scripts/rebuild-nodecg-native.mjs",
|
||||
"rebuild:better-sqlite3": "electron-rebuild --version 39.5.1 --module-dir lib/nodecg/workspaces/database-adapter-sqlite-legacy --only better-sqlite3 -f",
|
||||
"test": "npm run build && node --test dist/tests/**/*.test.js",
|
||||
"test": "rimraf dist && npm run build:main && node --test dist/tests/**/*.test.js",
|
||||
"doctor": "node scripts/doctor.mjs",
|
||||
"lint": "eslint . --ext .ts,.js,.mjs",
|
||||
"lint:fix": "npm run lint -- --fix",
|
||||
"format": "prettier --check .",
|
||||
"format:write": "prettier --write .",
|
||||
"dist:win": "npm run build && electron-builder --win",
|
||||
"dist:linux": "npm run build && electron-builder --linux AppImage",
|
||||
"dist:all": "npm run build && electron-builder --win --linux --mac",
|
||||
"dist:mac": "npm run build && electron-builder --mac"
|
||||
"dist:win": "npm run build && npm run rebuild:native && electron-builder --win",
|
||||
"dist:linux": "npm run build && npm run rebuild:native && electron-builder --linux AppImage",
|
||||
"dist:all": "npm run build && npm run rebuild:native && electron-builder --win --linux --mac",
|
||||
"dist:mac": "npm run build && npm run rebuild:native && electron-builder --mac"
|
||||
},
|
||||
"build": {
|
||||
"appId": "com.scoreko.desktop",
|
||||
@@ -71,7 +74,8 @@
|
||||
"nsis"
|
||||
],
|
||||
"icon": "static/icons/icon.ico",
|
||||
"executableName": "scoreko"
|
||||
"executableName": "scoreko",
|
||||
"signAndEditExecutable": false
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": false,
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env node
|
||||
import { existsSync, mkdirSync, rmSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { spawnSync } from "node:child_process";
|
||||
|
||||
const electronRoot = process.cwd();
|
||||
const bundleRoot = path.resolve(electronRoot, "..");
|
||||
const packageJsonPath = path.join(bundleRoot, "package.json");
|
||||
const pnpmLockPath = path.join(bundleRoot, "pnpm-lock.yaml");
|
||||
const nodeModulesPath = path.join(bundleRoot, "node_modules");
|
||||
|
||||
if (!existsSync(packageJsonPath) || !existsSync(pnpmLockPath)) {
|
||||
console.error(`Scoreko bundle root was not found at: ${bundleRoot}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!existsSync(nodeModulesPath)) {
|
||||
console.error(
|
||||
[
|
||||
"The Scoreko bundle dependencies are not installed.",
|
||||
`Run this once from ${bundleRoot}:`,
|
||||
" pnpm install",
|
||||
].join("\n"),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const generatedBundleEntries = ["extension", "node_modules/.vite", "shared/dist", "dashboard", "graphics"];
|
||||
const childEnv = {
|
||||
...process.env,
|
||||
COREPACK_HOME: process.env.COREPACK_HOME ?? path.join(electronRoot, ".corepack"),
|
||||
PATH: `${path.join(bundleRoot, "node_modules", ".bin")}${path.delimiter}${process.env.PATH ?? ""}`,
|
||||
};
|
||||
|
||||
function removeGeneratedOutput(relativePath) {
|
||||
const targetPath = path.resolve(bundleRoot, relativePath);
|
||||
|
||||
if (!targetPath.startsWith(`${bundleRoot}${path.sep}`)) {
|
||||
throw new Error(`Refusing to remove path outside the bundle root: ${targetPath}`);
|
||||
}
|
||||
|
||||
rmSync(targetPath, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
function runCommand(command, args) {
|
||||
const result = spawnSync(command, args, {
|
||||
cwd: bundleRoot,
|
||||
stdio: "inherit",
|
||||
shell: process.platform === "win32",
|
||||
env: childEnv,
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
console.error(`Could not run '${command} ${args.join(" ")}': ${result.error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (result.status !== 0) {
|
||||
process.exit(result.status ?? 1);
|
||||
}
|
||||
}
|
||||
|
||||
function runLocalBin(commandName, args) {
|
||||
const extension = process.platform === "win32" ? ".CMD" : "";
|
||||
runCommand(path.join(bundleRoot, "node_modules", ".bin", `${commandName}${extension}`), args);
|
||||
}
|
||||
|
||||
for (const entry of generatedBundleEntries) {
|
||||
removeGeneratedOutput(entry);
|
||||
}
|
||||
|
||||
for (const entry of ["shared/dist", "dashboard", "graphics", "extension"]) {
|
||||
mkdirSync(path.join(bundleRoot, entry), { recursive: true });
|
||||
}
|
||||
|
||||
runLocalBin("vite", ["build", "--configLoader", "runner"]);
|
||||
runLocalBin("tsc", ["-b", "tsconfig.extension.json"]);
|
||||
+9
-5
@@ -37,16 +37,20 @@ function parseIntInRange(name, fallback, min, max) {
|
||||
|
||||
function checkNodecgInstall() {
|
||||
const indexPath = path.join(nodecgRootPath, "index.js");
|
||||
const bootstrapPath = path.join(nodecgRootPath, "node_modules", "nodecg", "dist", "server", "bootstrap.js");
|
||||
const manifestPath = path.join(nodecgRootPath, ".scoreko-runtime.json");
|
||||
const bundleName = (process.env.NODECG_BUNDLE_NAME ?? "scoreko-dev").trim();
|
||||
const bundlePath = path.join(nodecgRootPath, "bundles", bundleName);
|
||||
|
||||
addCheck(fs.existsSync(nodecgRootPath), "NodeCG root", nodecgRootPath);
|
||||
addCheck(fs.existsSync(indexPath), "NodeCG index.js", indexPath);
|
||||
addCheck(fs.existsSync(bundlePath), `Bundle '${bundleName}'`, bundlePath);
|
||||
addCheck(fs.existsSync(nodecgRootPath), "Packaged NodeCG runtime", nodecgRootPath);
|
||||
addCheck(fs.existsSync(indexPath), "Runtime index.js", indexPath);
|
||||
addCheck(fs.existsSync(bootstrapPath), "NodeCG bootstrap", bootstrapPath);
|
||||
addCheck(fs.existsSync(manifestPath), "Runtime manifest", manifestPath);
|
||||
addCheck(fs.existsSync(bundlePath), `Packaged bundle '${bundleName}'`, bundlePath);
|
||||
|
||||
try {
|
||||
fs.accessSync(nodecgRootPath, fs.constants.R_OK | fs.constants.W_OK);
|
||||
addCheck(true, "lib/nodecg permissions", "Read/write OK");
|
||||
addCheck(true, "lib/nodecg permissions", "Read/write OK for local development");
|
||||
} catch {
|
||||
addCheck(false, "lib/nodecg permissions", "No read/write permissions in lib/nodecg");
|
||||
}
|
||||
@@ -82,7 +86,7 @@ async function main() {
|
||||
}
|
||||
|
||||
for (const check of checks) {
|
||||
const icon = check.ok ? "✅" : "❌";
|
||||
const icon = check.ok ? "OK" : "FAIL";
|
||||
console.log(`${icon} ${check.title}: ${check.details}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -1,15 +1,20 @@
|
||||
import { existsSync } from "node:fs";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { spawn } from "node:child_process";
|
||||
|
||||
const root = process.cwd();
|
||||
const nodecgDir = path.join(root, "lib", "nodecg");
|
||||
const sqliteLegacyDir = path.join(nodecgDir, "workspaces", "database-adapter-sqlite-legacy");
|
||||
const packageJson = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8"));
|
||||
const electronVersion = packageJson.devDependencies?.electron ?? packageJson.dependencies?.electron;
|
||||
const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm";
|
||||
|
||||
const moduleDirs = [nodecgDir, sqliteLegacyDir].filter((dir) => existsSync(path.join(dir, "package.json")));
|
||||
if (!electronVersion) {
|
||||
console.error("Could not determine Electron version from package.json.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (moduleDirs.length === 0) {
|
||||
console.error("No NodeCG package folders found. Expected lib/nodecg and/or workspaces.");
|
||||
if (!existsSync(path.join(nodecgDir, "package.json"))) {
|
||||
console.error("No packaged NodeCG runtime found. Run npm run prepare:runtime first.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -23,8 +28,10 @@ function run(command, args, cwd) {
|
||||
env: {
|
||||
...process.env,
|
||||
npm_config_runtime: "electron",
|
||||
npm_config_target: "39.5.1",
|
||||
npm_config_target: electronVersion,
|
||||
npm_config_disturl: "https://electronjs.org/headers",
|
||||
npm_config_cache: process.env.npm_config_cache ?? path.join(root, ".npm-runtime-cache"),
|
||||
ELECTRON_CACHE: process.env.ELECTRON_CACHE ?? path.join(root, ".electron-cache"),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -38,19 +45,13 @@ function run(command, args, cwd) {
|
||||
});
|
||||
}
|
||||
|
||||
for (const dir of moduleDirs) {
|
||||
if (dir === sqliteLegacyDir) {
|
||||
console.log(`\n[rebuild-native] Ensuring sqlite legacy workspace deps in: ${dir}`);
|
||||
await run("npm", ["install"], dir);
|
||||
await run("npm", ["install", "bindings", "--no-save"], dir);
|
||||
}
|
||||
|
||||
console.log(`\n[rebuild-native] Rebuilding better-sqlite3 in: ${dir}`);
|
||||
await run(
|
||||
"npm",
|
||||
["rebuild", "better-sqlite3", "--runtime=electron", "--target=39.5.1", "--dist-url=https://electronjs.org/headers"],
|
||||
dir,
|
||||
);
|
||||
}
|
||||
console.log(`\n[rebuild-native] Rebuilding better-sqlite3 for Electron ${electronVersion} in: ${nodecgDir}`);
|
||||
await run(npmCommand, [
|
||||
"rebuild",
|
||||
"better-sqlite3",
|
||||
"--runtime=electron",
|
||||
`--target=${electronVersion}`,
|
||||
"--dist-url=https://electronjs.org/headers",
|
||||
], nodecgDir);
|
||||
|
||||
console.log("\n[rebuild-native] Done.");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const NODE_RUNTIME_NAME = "electron internal node";
|
||||
export const NODE_RUNTIME_NAME = "Electron embedded Node.js";
|
||||
export const DEFAULT_WINDOW_BACKGROUND = "#0f0f0f";
|
||||
|
||||
export const DEFAULT_WINDOW_SIZE = {
|
||||
|
||||
+22
-12
@@ -3,7 +3,8 @@ import path from "node:path";
|
||||
|
||||
import { getRuntimeConfig } from "./config/runtime-config";
|
||||
import { showFatalError, log } from "./errors/error-presenter";
|
||||
import { createNodecgProcessManager } from "./nodecg/process-manager";
|
||||
import { createNodecgProcessManager, NodecgProcessManager } from "./nodecg/process-manager";
|
||||
import { prepareUserNodecgRuntime } from "./nodecg/runtime-provisioner";
|
||||
import { getRemainingDelayMs } from "./utils/timing";
|
||||
import { createLoadingWindow, createMainWindow } from "./windows/window-factory";
|
||||
|
||||
@@ -15,7 +16,7 @@ app.setPath("userData", path.join(app.getPath("appData"), appConfig.userDataDire
|
||||
|
||||
const isDev = !app.isPackaged;
|
||||
const rootPath = isDev ? path.resolve(__dirname, "../..") : process.resourcesPath;
|
||||
const nodecgRootPath = path.resolve(rootPath, "lib", "nodecg");
|
||||
const sourceNodecgRuntimePath = path.resolve(rootPath, "lib", "nodecg");
|
||||
const mainDashboardUrl = `http://localhost:${appConfig.nodecgPort}/bundles/${appConfig.bundleName}/${appConfig.mainDashboardRoute}`;
|
||||
const loadingDashboardUrl = `http://localhost:${appConfig.nodecgPort}/bundles/${appConfig.bundleName}/${appConfig.loadingDashboardRoute}`;
|
||||
const nodecgBaseUrl = `http://127.0.0.1:${appConfig.nodecgPort}`;
|
||||
@@ -26,18 +27,11 @@ if (!hasSingleInstanceLock) {
|
||||
app.quit();
|
||||
}
|
||||
|
||||
const nodecgManager = createNodecgProcessManager({
|
||||
isDev,
|
||||
nodecgRootPath,
|
||||
nodecgBaseUrl,
|
||||
appConfig,
|
||||
log,
|
||||
});
|
||||
|
||||
type AppShutdownState = "running" | "stopping" | "stopped";
|
||||
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
let loadingWindow: BrowserWindow | null = null;
|
||||
let nodecgManager: NodecgProcessManager | null = null;
|
||||
let shutdownState: AppShutdownState = "running";
|
||||
|
||||
function focusExistingWindow(): void {
|
||||
@@ -56,6 +50,22 @@ function focusExistingWindow(): void {
|
||||
}
|
||||
|
||||
async function launchApplication(): Promise<void> {
|
||||
const nodecgRootPath = prepareUserNodecgRuntime({
|
||||
sourceRuntimePath: sourceNodecgRuntimePath,
|
||||
userDataPath: app.getPath("userData"),
|
||||
appVersion: app.getVersion(),
|
||||
bundleName: appConfig.bundleName,
|
||||
log,
|
||||
});
|
||||
|
||||
nodecgManager = createNodecgProcessManager({
|
||||
isDev,
|
||||
nodecgRootPath,
|
||||
nodecgBaseUrl,
|
||||
appConfig,
|
||||
log,
|
||||
});
|
||||
|
||||
// We create both windows early so startup feels instant while NodeCG is booting in the background.
|
||||
mainWindow = createMainWindow({ appConfig, rootPath, mainDashboardUrl });
|
||||
loadingWindow = createLoadingWindow({ appConfig, rootPath });
|
||||
@@ -110,12 +120,12 @@ function stopNodecgGracefully(): Promise<void> {
|
||||
}
|
||||
|
||||
if (shutdownState === "stopping") {
|
||||
return nodecgManager.stopNodecgProcessGracefully();
|
||||
return nodecgManager?.stopNodecgProcessGracefully() ?? Promise.resolve();
|
||||
}
|
||||
|
||||
shutdownState = "stopping";
|
||||
|
||||
return nodecgManager.stopNodecgProcessGracefully().finally(() => {
|
||||
return (nodecgManager?.stopNodecgProcessGracefully() ?? Promise.resolve()).finally(() => {
|
||||
shutdownState = "stopped";
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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")));
|
||||
});
|
||||
Reference in New Issue
Block a user