feat: bulletproof updater + fixes for client links (icons/furniture/gamedata)
This commit is contained in:
1 parent
f2ac4745d4
commit
678d22ceab
3 files changed
+346
-74
No files matched your search
+49
-10
@@ -8,19 +8,30 @@ if (!exampleFile || !targetFile) {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isPlainObject(value) {
|
||||||
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge the upstream example into the live config with strict "add missing,
|
||||||
|
// never remove or overwrite" semantics so hotel configs survive every update:
|
||||||
|
// - every key already present in the live config is PRESERVED (scalars,
|
||||||
|
// arrays and nested objects always win over the upstream example),
|
||||||
|
// - nested objects are merged recursively so upstream-only sub-keys are
|
||||||
|
// added while the hotel's own sub-keys stay untouched,
|
||||||
|
// - keys that do not exist in the live config yet are ADDED from the example,
|
||||||
|
// - a source object NEVER replaces an existing scalar/array (would corrupt).
|
||||||
function deepMerge(target, source) {
|
function deepMerge(target, source) {
|
||||||
const result = { ...target };
|
const result = { ...target };
|
||||||
for (const key of Object.keys(source)) {
|
for (const key of Object.keys(source)) {
|
||||||
if (
|
if (isPlainObject(source[key])) {
|
||||||
source[key] !== null &&
|
if (isPlainObject(result[key])) {
|
||||||
typeof source[key] === "object" &&
|
result[key] = deepMerge(result[key], source[key]);
|
||||||
!Array.isArray(source[key])
|
} else if (!(key in result)) {
|
||||||
) {
|
result[key] = deepMerge({}, source[key]);
|
||||||
result[key] = deepMerge(target[key] || {}, source[key]);
|
|
||||||
} else {
|
|
||||||
if (!(key in result)) {
|
|
||||||
result[key] = source[key];
|
|
||||||
}
|
}
|
||||||
|
// Else: live value is a scalar/array/null -> keep it untouched.
|
||||||
|
} else if (!(key in result)) {
|
||||||
|
result[key] = source[key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
@@ -69,4 +80,32 @@ try {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const merged = deepMerge(current, example);
|
const merged = deepMerge(current, example);
|
||||||
fs.writeFileSync(targetFile, `${JSON.stringify(merged, null, 4)}\n`);
|
|
||||||
|
// Sanity: the merged result must stay an object and the written file must be
|
||||||
|
// re-parseable. If anything goes wrong we keep the live file untouched rather
|
||||||
|
// than shipping a corrupted config to the client.
|
||||||
|
if (merged === undefined || merged === null || typeof merged !== "object") {
|
||||||
|
process.stderr.write(
|
||||||
|
`[merge-config] refused to write non-object result for ${targetFile}\n`,
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the result parses round-trip before overwriting the live config.
|
||||||
|
const serialized = `${JSON.stringify(merged, null, 4)}\n`;
|
||||||
|
let check;
|
||||||
|
try {
|
||||||
|
check = parse(serialized, [], {
|
||||||
|
allowTrailingComma: true,
|
||||||
|
allowEmptyContent: true,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
check = undefined;
|
||||||
|
}
|
||||||
|
if (!check || typeof check !== "object" || Array.isArray(check)) {
|
||||||
|
process.stderr.write(
|
||||||
|
`[merge-config] refused to write unparseable result for ${targetFile}\n`,
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
fs.writeFileSync(targetFile, serialized);
|
||||||
@@ -13,9 +13,10 @@
|
|||||||
// NITRO_IMAGE_LIBRARY_URL, NITRO_HOF_FURNITURE_URL, NITRO_API_URL,
|
// NITRO_IMAGE_LIBRARY_URL, NITRO_HOF_FURNITURE_URL, NITRO_API_URL,
|
||||||
// NITRO_SOCKET_URL, NITRO_GAMEDATA_URL, NITRO_ASSET_URL,
|
// NITRO_SOCKET_URL, NITRO_GAMEDATA_URL, NITRO_ASSET_URL,
|
||||||
// NITRO_FURNI_ASSET_ICON_URL
|
// NITRO_FURNI_ASSET_ICON_URL
|
||||||
|
// NITRO_URL_FORCE=1 (optional) overwrite existing values too
|
||||||
const fs = require("node:fs");
|
const fs = require("node:fs");
|
||||||
const path = require("node:path");
|
const path = require("node:path");
|
||||||
const { parse, modify } = require("jsonc-parser");
|
const { parse } = require("jsonc-parser");
|
||||||
|
|
||||||
const FILES = [
|
const FILES = [
|
||||||
"renderer-config.json",
|
"renderer-config.json",
|
||||||
@@ -23,6 +24,22 @@ const FILES = [
|
|||||||
"ui-config.json",
|
"ui-config.json",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const FORCE = process.env.NITRO_URL_FORCE === "1";
|
||||||
|
|
||||||
|
// Values that must never survive into a production config: placeholder or
|
||||||
|
// loopback hosts that originate from the upstream example files. When a key
|
||||||
|
// still holds one of these, it has been newly added by the merge step and has
|
||||||
|
// to be filled from the environment. A real, already-configured URL (the
|
||||||
|
// hotel's own domain) is NEVER rewritten, so hotel links stay exactly as the
|
||||||
|
// hotel owner configured them.
|
||||||
|
const PLACEHOLDER_RE =
|
||||||
|
/(localhost|127\.0\.0\.1|0\.0\.0\.0|::1|\.example\.com|nitro\.example\.|hotel\.example\.|example\.org|example\.net|:5173|:2096|\$\{)/i;
|
||||||
|
|
||||||
|
function isPlaceholder(value) {
|
||||||
|
if (!value) return true;
|
||||||
|
return PLACEHOLDER_RE.test(value);
|
||||||
|
}
|
||||||
|
|
||||||
function env(name) {
|
function env(name) {
|
||||||
return process.env[name]?.length ? process.env[name] : null;
|
return process.env[name]?.length ? process.env[name] : null;
|
||||||
}
|
}
|
||||||
@@ -36,9 +53,14 @@ function applyOverrides(data) {
|
|||||||
const asset = env("NITRO_ASSET_URL");
|
const asset = env("NITRO_ASSET_URL");
|
||||||
const icon = env("NITRO_FURNI_ASSET_ICON_URL");
|
const icon = env("NITRO_FURNI_ASSET_ICON_URL");
|
||||||
|
|
||||||
|
// Add-only, placeholder-aware URL fill-in. Existing real values are kept,
|
||||||
|
// crypto.* keys are never touched, and only missing/placeholder keys are
|
||||||
|
// filled from the environment. NITRO_URL_FORCE=1 opts back into overwriting.
|
||||||
const set = (key, value) => {
|
const set = (key, value) => {
|
||||||
if (value && !(key in data)) data[key] = value;
|
if (!value || key.startsWith("crypto.")) return;
|
||||||
if (value) data[key] = value;
|
if (FORCE || !(key in data) || isPlaceholder(data[key])) {
|
||||||
|
data[key] = value;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
set("image.library.url", image);
|
set("image.library.url", image);
|
||||||
@@ -49,12 +71,19 @@ function applyOverrides(data) {
|
|||||||
set("asset.url", asset);
|
set("asset.url", asset);
|
||||||
set("furni.asset.icon.url", icon);
|
set("furni.asset.icon.url", icon);
|
||||||
|
|
||||||
if (gamedata) {
|
if (gamedata && (!("radio.url" in data) || isPlaceholder(data["radio.url"]))) {
|
||||||
data["radio.url"] = `${gamedata}/config/radio-stations.jsonc?t=%timestamp%`;
|
data["radio.url"] = `${gamedata}/config/radio-stations.jsonc?t=%timestamp%`;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
gamedata &&
|
||||||
|
(!("soundboard.url" in data) || isPlaceholder(data["soundboard.url"]))
|
||||||
|
) {
|
||||||
data["soundboard.url"] =
|
data["soundboard.url"] =
|
||||||
`${gamedata}/config/soundboard-sounds.jsonc?t=%timestamp%`;
|
`${gamedata}/config/soundboard-sounds.jsonc?t=%timestamp%`;
|
||||||
}
|
}
|
||||||
if ("show.google.ads" in data) data["show.google.ads"] = false;
|
// Never force ads on/off over a hotel's own choice; only default to "off"
|
||||||
|
// when the key does not exist yet (prevents external ad scripts from 404ing).
|
||||||
|
if (!("show.google.ads" in data)) data["show.google.ads"] = false;
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+263
-59
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user