diff --git a/scripts/merge-config.cjs b/scripts/merge-config.cjs index 91a3cc2e5d..48c15bd519 100644 --- a/scripts/merge-config.cjs +++ b/scripts/merge-config.cjs @@ -1,5 +1,5 @@ const fs = require("node:fs"); -const JSON5 = require("json5"); +const { parse, printParseErrorCode } = require("jsonc-parser"); const exampleFile = process.argv[2]; const targetFile = process.argv[3]; @@ -26,28 +26,47 @@ function deepMerge(target, source) { return result; } +function parseJsonc(file) { + if (!fs.existsSync(file)) { + return undefined; + } + const content = fs.readFileSync(file, "utf-8"); + const errors = []; + const value = parse(content, errors, { + allowTrailingComma: true, + allowEmptyContent: true, + }); + if (errors.length > 0) { + console.error( + `[merge-config] parse error in ${file}: ${errors + .map((e) => printParseErrorCode(e.error)) + .join(", ")}`, + ); + return undefined; + } + return value; +} + let example; try { - const content = fs.readFileSync(exampleFile, "utf-8"); - example = JSON5.parse(content); + example = parseJsonc(exampleFile); } catch { process.exit(1); } +if (example === undefined) { + process.exit(1); +} + let current = {}; try { - if (fs.existsSync(targetFile)) { - const content = fs.readFileSync(targetFile, "utf-8"); - current = JSON5.parse(content); + const parsed = parseJsonc(targetFile); + if (parsed !== undefined && parsed !== null) { + current = parsed; } } catch { current = {}; } const merged = deepMerge(current, example); - -const isJson5 = targetFile.endsWith(".json5"); -const output = isJson5 - ? JSON5.stringify(merged, null, 4) - : JSON.stringify(merged, null, 4); -fs.writeFileSync(targetFile, `${output}\n`); +fs.writeFileSync(targetFile, `${JSON.stringify(merged, null, 4)}\n`); diff --git a/update-Nitrov3.sh b/update-Nitrov3.sh index 7fed650f41..3b13047433 100755 --- a/update-Nitrov3.sh +++ b/update-Nitrov3.sh @@ -116,6 +116,13 @@ BACKUP_DIR="${NITRO_BACKUP_DIR:-$EMULATOR_DIR/Database/Database Updates/backups} NITRO_CLIENT="${NITRO_CLIENT_SRC:-/var/www/Nitro-V3}" NITRO_RENDERER="${NITRO_RENDERER_SRC:-/var/www/Nitro_Render_V3}" NITRO_BRANCH="${NITRO_BRANCH:-}" +# Dedicated per-repo yarn cache folders. The renderer + client installs run in +# parallel and yarn 1.22 has no cache locking, so a single shared cache gets +# corrupted and node_modules is left incomplete (vite/pixi.js missing) while +# yarn still exits 0. Isolating each repo's cache via --cache-folder prevents +# that race entirely. +YARN_CACHE_DIR="${NITRO_YARN_CACHE_DIR:-/var/www/.yarn-cache}" +mkdir -p "$YARN_CACHE_DIR" 2>/dev/null || true MIN_DISK_GB="${NITRO_MIN_DISK_GB:-5}" HEALTH_RETRIES="${NITRO_HEALTH_RETRIES:-12}" HEALTH_INTERVAL="${NITRO_HEALTH_INTERVAL:-5}" @@ -925,6 +932,30 @@ clean_node_modules() { rm -rf "node_modules" 2>/dev/null || sudo rm -rf "$(pwd)/node_modules" 2>/dev/null || true } +# yarn install scoped to a per-repo cache folder. Parallel installs (renderer + +# client) must never share a cache — yarn 1 corrupts the shared cache, leaving +# node_modules incomplete (vite/pixi.js missing) while still exiting 0. +# $1 = repo dir; remaining args forwarded to yarn. Runs inside the repo and +# always returns the caller to SCRIPT_DIR so cwd never leaks between repos. +yarn_install_repo() { + local repo="$1"; shift + local cache_dir="$YARN_CACHE_DIR/$(basename "$repo")" + mkdir -p "$cache_dir" 2>/dev/null || true + cd "$repo" 2>/dev/null || { warn "Cannot cd into %s" "$repo"; return 1; } + yarn install --cache-folder "$cache_dir" "$@" >> "$LOG_FILE" 2>&1 + local rc=$? + cd "$SCRIPT_DIR" 2>/dev/null || true + return $rc +} + +# Nuke a repo's own yarn cache and node_modules for a truly clean reinstall. +# Parallel installs may have corrupted the cache, so it must be discarded too. +yarn_reset_repo() { + local repo="$1" + rm -rf "$repo/node_modules" "$YARN_CACHE_DIR/$(basename "$repo")" 2>/dev/null || true + mkdir -p "$YARN_CACHE_DIR/$(basename "$repo")" 2>/dev/null || true +} + # ============================================================================= # PARALLEL HELPERS # ============================================================================= @@ -1130,17 +1161,18 @@ update_renderer() { HAD_UPDATES=true; UPDATED_REPOS+=("Nitro_Render_V3") cd "$NITRO_RENDERER" spinner_start "Installing deps..." - if ! yarn install --frozen-lockfile --prefer-offline >> "$LOG_FILE" 2>&1; then + if ! yarn_install_repo "$NITRO_RENDERER" --frozen-lockfile --prefer-offline; then warn "Renderer: yarn install failed — cleaning node_modules and retrying" - clean_node_modules - yarn install >> "$LOG_FILE" 2>&1 || { spinner_stop fail; die "yarn install failed (see: %s)" "$LOG_FILE"; } + yarn_reset_repo "$NITRO_RENDERER" + yarn_install_repo "$NITRO_RENDERER" || { spinner_stop fail; die "yarn install failed (see: %s)" "$LOG_FILE"; } fi + cd "$NITRO_RENDERER" if [ ! -d node_modules/pixi.js ]; then - warn "Renderer: node_modules incomplete — cleaning and doing full install" - clean_node_modules - # Parallel yarn installs (renderer + client) can corrupt the shared - # yarn cache, so force a clean fetch with --no-cache as a last resort. - yarn install --no-cache >> "$LOG_FILE" 2>&1 || { spinner_stop fail; die "yarn install failed (see: %s)" "$LOG_FILE"; } + warn "Renderer: node_modules incomplete — resetting repo cache and doing full install" + # Parallel yarn installs can corrupt a shared cache; resetting the + # repo's own cache folder guarantees a clean fetch from the registry. + yarn_reset_repo "$NITRO_RENDERER" + yarn_install_repo "$NITRO_RENDERER" || { spinner_stop fail; die "yarn install failed (see: %s)" "$LOG_FILE"; } fi spinner_stop ok; ok "Renderer dependencies installed" else @@ -1160,17 +1192,19 @@ update_client() { spinner_start "Installing deps..." # yarn 1 can "succeed" while leaving node_modules incomplete, so verify # that the vite build binary actually exists before attempting a build. - if ! yarn install --frozen-lockfile --prefer-offline >> "$LOG_FILE" 2>&1; then + if ! yarn_install_repo "$NITRO_CLIENT" --frozen-lockfile --prefer-offline; then warn "Nitro-V3: yarn install failed — cleaning node_modules and retrying" - clean_node_modules - yarn install >> "$LOG_FILE" 2>&1 || { spinner_stop fail; die "yarn install failed (see: %s)" "$LOG_FILE"; } + yarn_reset_repo "$NITRO_CLIENT" + yarn_install_repo "$NITRO_CLIENT" || { spinner_stop fail; die "yarn install failed (see: %s)" "$LOG_FILE"; } fi + cd "$NITRO_CLIENT" if [ ! -x node_modules/.bin/vite ]; then - warn "Nitro-V3: vite missing after install — cleaning node_modules and doing full install" - clean_node_modules - # Parallel yarn installs (renderer + client) can corrupt the shared - # yarn cache, so force a clean fetch with --no-cache as a last resort. - yarn install --no-cache >> "$LOG_FILE" 2>&1 || { spinner_stop fail; die "yarn install failed (see: %s)" "$LOG_FILE"; } + warn "Nitro-V3: vite missing after install — resetting repo cache and doing full install" + # Parallel yarn installs can corrupt a shared cache; resetting the + # repo's own cache folder guarantees a clean fetch from the registry. + yarn_reset_repo "$NITRO_CLIENT" + yarn_install_repo "$NITRO_CLIENT" || { spinner_stop fail; die "yarn install failed (see: %s)" "$LOG_FILE"; } + cd "$NITRO_CLIENT" fi spinner_stop ok spinner_start "Building frontend..." @@ -1293,7 +1327,10 @@ do_cleanup() { ok "Old config backups pruned" fi [ -d "$BACKUP_DIR" ] && find "$BACKUP_DIR" -maxdepth 1 -name '*.sql' -printf '%T@ %p\n' 2>/dev/null | sort -rn | tail -n +6 | sed 's/^[0-9.]* //' | xargs -r rm -f || true + # Clean both the global yarn cache and the per-repo caches used by the + # parallel renderer/client installs. yarn cache clean 2>/dev/null || true + rm -rf "$YARN_CACHE_DIR" 2>/dev/null || true rm -rf /tmp/nitro-* /tmp/epicnext-* 2>/dev/null || true # Clean old Maven build artifacts (older than 7 days) find "$EMULATOR_MODULE" -maxdepth 3 -name '*.jar' -mtime +7 2>/dev/null -exec rm -f {} \; || true @@ -1459,7 +1496,9 @@ cmd_update() { echo "" acquire_lock DRY_RUN=${DRY_RUN:-false} - info "Site: %s | Branch: %s | Mode: %s" "${NITRO_SITE_URL:-?}" "$NITRO_BRANCH" "${SELECTIVE_UPDATE:-full}${DRY_RUN:+ [DRY-RUN]}" + local mode_label="${SELECTIVE_UPDATE:-full}" + [ "$DRY_RUN" = true ] && mode_label="$mode_label [DRY-RUN]" + info "Site: %s | Branch: %s | Mode: %s" "${NITRO_SITE_URL:-?}" "$NITRO_BRANCH" "$mode_label" info "Log: %s" "$LOG_FILE" if [ "$DRY_RUN" = true ]; then info "Dry-run: would update repos, run DB backups, build, sync configs, cleanup, restart services" @@ -1485,17 +1524,17 @@ cmd_update() { *) RUN_ROLLBACK=true update_emulator - if [ "$HAD_UPDATES" = true ]; then - parallel_run "renderer" update_renderer - parallel_run "client" update_client - parallel_wait - if ! parallel_failed; then - warn "Parallel build(s) failed: %s — aborting before restart" "${PARALLEL_FAILED[*]}" - ROLLBACK_NEEDED=true - die "Build failure in: %s" "${PARALLEL_FAILED[*]}" - fi - else - info "No repo updates; skipping parallel builds" + # Always attempt the renderer + client builds in parallel. Each + # update_* function runs git_update itself and skips when its repo + # is already up to date, so gating these on the emulator's update + # status would wrongly skip them when only client/renderer changed. + parallel_run "renderer" update_renderer + parallel_run "client" update_client + parallel_wait + if ! parallel_failed; then + warn "Parallel build(s) failed: %s — aborting before restart" "${PARALLEL_FAILED[*]}" + ROLLBACK_NEEDED=true + die "Build failure in: %s" "${PARALLEL_FAILED[*]}" fi sync_configs do_cleanup @@ -1735,14 +1774,14 @@ cmd_flyaway_repair() { # Reinstall dependencies if package.json exists if [ -f "$repo/package.json" ]; then spinner_start "Installing deps for %s..." "$name" - if yarn install --frozen-lockfile --prefer-offline 2>/dev/null; then + if yarn_install_repo "$repo" --frozen-lockfile --prefer-offline 2>/dev/null; then spinner_stop ok ok "Deps reinstalled for %s" "$name" else spinner_stop fail - warn "yarn install failed for %s — cleaning node_modules and retrying" "$name" - rm -rf "$repo/node_modules" 2>/dev/null || true - yarn install --frozen-lockfile --prefer-offline 2>/dev/null || warn "yarn install still failed for %s" "$name" + warn "yarn install failed for %s — resetting repo and retrying" "$name" + yarn_reset_repo "$repo" + yarn_install_repo "$repo" --frozen-lockfile --prefer-offline 2>/dev/null || warn "yarn install still failed for %s" "$name" fi fi repaired=$((repaired + 1)) @@ -1917,11 +1956,11 @@ cmd_clean() { echo -e " ${C_BOLD}7)${C_RESET} $(_t "Maven") ${C_BOLD}0)${C_RESET} $(_t "Return")" echo -en "\n $(_t "Select:") "; local c; read -rt 30 c case "$c" in - 1) yarn cache clean 2>/dev/null && ok "Yarn cleaned" ;; + 1) yarn cache clean 2>/dev/null; rm -rf "$YARN_CACHE_DIR" 2>/dev/null; ok "Yarn cleaned" ;; 2) cd "$SCRIPT_DIR" && rm -rf .next 2>/dev/null && ok "Next.js cache cleared" ;; 3) redis-cli FLUSHALL 2>/dev/null && ok "Redis flushed" || warn "Redis N/A" ;; 4) find "$EMULATOR_DIR" -name "*.log" -mtime +14 -exec rm -f {} \; 2>/dev/null; ok "Logs cleaned" ;; - 5) yarn cache clean 2>/dev/null; find "$EMULATOR_DIR" -name "*.log" -mtime +14 -exec rm -f {} \; 2>/dev/null; rm -rf /tmp/nitro-* /tmp/epicnext-* 2>/dev/null; ok "Full cleanup" ;; + 5) yarn cache clean 2>/dev/null; rm -rf "$YARN_CACHE_DIR" 2>/dev/null; find "$EMULATOR_DIR" -name "*.log" -mtime +14 -exec rm -f {} \; 2>/dev/null; rm -rf /tmp/nitro-* /tmp/epicnext-* 2>/dev/null; ok "Full cleanup" ;; 6) command -v docker &>/dev/null && docker system prune -f 2>/dev/null && ok "Docker pruned" || warn "Docker N/A" ;; 7) find "$EMULATOR_MODULE" -maxdepth 3 -name '*.jar' -mtime +7 -exec rm -f {} \; 2>/dev/null; ok "Old Maven artifacts cleaned" ;; esac