/** * Visible renderer-load error page. * * The white-screen failure modes this exists for: * * 1. TORN BUNDLE after an update (#95575): `hermes update` replaces the app * while its files are locked (antivirus, a still-running instance, an * interrupted Windows replace), leaving index.html and its hashed chunks * from DIFFERENT generations. The window loads, then dies on the first * lazy import ("Failed to fetch dynamically imported module") — a white * screen that no amount of restarting fixes. * 2. LOAD FAILURE: a missing/blocked index.html surfaces only as a bare * ERR_FILE_NOT_FOUND window (see #39484) with a log line nobody sees. * * Both used to leave the user staring at a blank window with the only * explanation in `logs/desktop.log`. This module renders the failure INTO * the window — error code, what is missing, how to repair — with a Reload * button, so the white screen becomes a diagnosable, actionable surface. * * Pure + injectable so it is testable without booting Electron: the page is * a self-contained data: URL (no network, no file access), so `loadURL` can * never itself fail on a torn install. */ export interface RendererLoadErrorDetails { /** Chromium error code, e.g. -6 (ERR_FILE_NOT_FOUND) or its name. */ errorCode?: number | string | undefined /** Human description of the failure, e.g. the renderer bundle is torn. */ errorDescription?: string /** The URL that failed to load, when known. */ url?: string /** Module files index.html declares but that are missing on disk. */ missingAssets?: string[] /** Repair command hint, e.g. `hermes desktop --force-build`. */ repairHint?: string /** * URL to navigate to when the user clicks Reload. On a data: page * `location.reload()` would just re-render the error page, so recovery * must target the real renderer URL. Omitted → the button reloads in * place (harmless: the caller's load-failure policy re-surfaces). */ reloadUrl?: string } /** * Escape a ``JSON.stringify`` result for embedding inside an inline * ``` ) } function escapeHtml(value: unknown): string { return String(value ?? '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') } function missingAssetsList(missingAssets?: string[]): string { const assets = (missingAssets ?? []).slice(0, 5) if (assets.length === 0) { return '' } const items = assets.map(asset => `
  • ${escapeHtml(asset)}
  • `).join('') return ( `

    The renderer bundle is missing ${missingAssets!.length} module file(s) ` + `(first ${assets.length} shown) — the last update replaced the app while ` + `its files were locked.

    ` ) } /** * Build the self-contained error page. Deliberately dependency-free: no * stylesheets, no images, no fetch — a data: URL must render from a blank * origin with zero network access. */ export function buildRendererLoadErrorPage(details: RendererLoadErrorDetails = {}): string { const code = details.errorCode === undefined || details.errorCode === null ? '' : ` (${escapeHtml(details.errorCode)})` const title = 'Hermes couldn\u2019t start the desktop UI' const description = escapeHtml(details.errorDescription || 'The desktop renderer failed to load.') const url = details.url ? `

    ${escapeHtml(details.url)}

    ` : '' const repair = details.repairHint ? `

    Repair with: hermes desktop --force-build

    ` : '' return ` ${title}

    ${title}

    ${description}${code}

    ${url} ${missingAssetsList(details.missingAssets)} ${repair}

    If this keeps happening, check logs/desktop.log and try hermes desktop --force-build, then restart the app.

    ${reloadButtonJs(details)}
    ` } /** Minimal structural surface of BrowserWindow used here. */ export interface LoadErrorWindowLike { loadURL: (url: string) => Promise } const DATA_URL_PREFIX = 'data:text/html;charset=utf-8,' /** * Load the visible error page into a window, replacing the white screen. * Always resolves — loadURL is the one call that could reject, and a * rejection must not be allowed to turn the recovery surface itself blank. */ export async function loadRendererLoadErrorPage( win: LoadErrorWindowLike, details: RendererLoadErrorDetails = {} ): Promise { const url = `${DATA_URL_PREFIX}${encodeURIComponent(buildRendererLoadErrorPage(details))}` try { await win.loadURL(url) } catch { // The white screen is strictly better than an unhandled rejection here; // the log line from the caller still tells the story. } }