// Build-time guard: refuse to hand a half-built renderer to electron-builder. // // `npm run pack` / `npm run dist*` are `npm run build && npm run builder`. // If the `build` step (tsc -b && vite build) fails but packaging proceeds // anyway — a stale checkout that fails typecheck, an interrupted vite build, // or npm not short-circuiting `&&` in some shells — electron-builder happily // packages an app with an empty or missing `dist/`. The result launches but // blank-pages with `ERR_FILE_NOT_FOUND` for dist/index.html, with no clue why. // // This runs at the tail of `build`, after vite build, so any packaging path // inherits it. It fails loud and early instead of shipping a broken bundle. // See issues #39484 (renderer blank page) and #41327 / #39472 (dashboard 404). import { existsSync, readFileSync, statSync, readdirSync } from "fs" import { spawnSync } from "child_process" import { join, resolve } from "path" import { isMain } from "./utils.mjs" const ROUTER_CONTEXT_ERROR = "may be used only in the context of a" // @tanstack/react-query carries module-level React context (QueryClientContext). // The entry's QueryClientProvider and every lazy chunk's useQuery must share ONE // runtime instance; if a build ever emits a second copy, the provider's context // is invisible to the other copy and useQuery throws "No QueryClient set" — the // packaged app error-boundaries on launch (#95560). Same single-instance // invariant as the react-router check above, same failure class. const QUERY_CLIENT_CONTEXT_ERROR = "No QueryClient set, use QueryClientProvider to set one" // Pure check — returns { ok: true } or { ok: false, error: "..." }. // Kept side-effect-free so it can be unit tested without spawning a process. export function checkDistBuilt(distDir) { if (!existsSync(distDir) || !statSync(distDir).isDirectory()) { return { ok: false, error: `no dist directory at ${distDir}` } } const indexHtml = join(distDir, "index.html") if (!existsSync(indexHtml) || !statSync(indexHtml).isFile()) { return { ok: false, error: `dist/index.html is missing at ${indexHtml}` } } if (statSync(indexHtml).size === 0) { return { ok: false, error: `dist/index.html is empty at ${indexHtml}` } } // index.html alone isn't enough — vite emits hashed JS into dist/assets. // An index.html with no script bundle still blank-pages. const assetsDir = join(distDir, "assets") const hasAssets = existsSync(assetsDir) && statSync(assetsDir).isDirectory() && readdirSync(assetsDir).some(name => name.endsWith(".js")) if (!hasAssets) { return { ok: false, error: `dist/assets has no built JS bundle (expected vite output under ${assetsDir})` } } const routerContextAssets = readdirSync(assetsDir) .filter(name => name.endsWith(".js")) .filter(name => readFileSync(join(assetsDir, name), "utf8").includes(ROUTER_CONTEXT_ERROR)) if (routerContextAssets.length > 1) { return { ok: false, error: `react-router context invariant found in multiple JS assets: ${routerContextAssets.join(", ")}` } } const queryClientContextAssets = readdirSync(assetsDir) .filter(name => name.endsWith(".js")) .filter(name => readFileSync(join(assetsDir, name), "utf8").includes(QUERY_CLIENT_CONTEXT_ERROR)) if (queryClientContextAssets.length > 1) { return { ok: false, error: `@tanstack/react-query context invariant found in multiple JS assets: ` + `${queryClientContextAssets.join(", ")} — duplicate react-query runtimes make the ` + `QueryClientProvider's context invisible to useQuery in other chunks (` + `"No QueryClient set" on launch, #95560)` } } // Parse-validate every emitted chunk as an ES module. Corrupted-silent-fail // bundles (a dropped identifier token mid-file) produce invalid syntax that // only explodes at module-evaluation time in Electron's renderer. const chunkParse = verifyChunksParse(assetsDir) if (!chunkParse.ok) { return chunkParse } return { ok: true } } // Renderer chunks are emitted as ESM (`