import React from "react"; import { createRoot, type Root } from "react-dom/client"; import "./index.css"; import PreviewBootError from "./components/PreviewBootError"; // Runtime guard: clamp invalid Element.animate() iterations so one bad // AI-generated motion.* transition never blanks the whole page. // Must patch Element.prototype before any framer-motion code runs. import "./lib/animateGuard"; // Quiet-HMR bridge — listens for lps:quiet:* HMR events from the // dev server plugin and forwards to the HIDE_VITE_ERROR_OVERLAY // postMessage handler in index.html. Loaded before React mounts so // the overlay is hidden before any transform failure during a bulk // agent-edit window. import "./lib/quietHmr"; // Load custom embeddings (analytics, tracking pixels, etc.) // Must be imported before App to ensure scripts run early import "./lib/EmbeddingsLoader"; // Visual editor inspector (handles element selection in iframe) import "./lib/inspector"; // LPS-903 — forwards Vite HMR connect/disconnect to the parent so // PreviewFrame can react instantly when the dev server dies (pod // evicted, OOM, Vite crash) instead of waiting for Django's status push. import "./lib/previewHealthEmitter"; // LPS-943 — `PerformanceDefaults` was removed: it ran client-side after React // hydration, mutating `` on every Unsplash image AFTER the browser // had already started fetching the original. That caused a second HTTP // request per image and a `` injected after `load` // (useless for LCP). Image optimisation now happens at SSR/build time in // `scripts/prerender.mjs` where the bytes can actually be saved before they // hit the wire. // LPS Validation Phase 3 — permanent runtime error observer. // Generalizes the LPS-700 boot-window observer into a session-wide observer. // Captures window.error, unhandledrejection, blank-#root, ReportingObserver // and POSTs batches to /api/projects/{id}/runtime-errors/ via telemetry_client. // The legacy APP_BOOT_FAILED postMessage path below is preserved as a // parallel fallback signal (per Phase 3 D-13) and will be removed in a // later phase once the new path is proven. import { installErrorObserver } from "./lib/error_observer"; import { installAuthBridge } from "./lib/auth_bridge"; installAuthBridge(); // Upload projects (react_source / static_html / static_zip) cold-start with a // heavier first-paint graph than AI-generated sites — real hero imagery, full // route table, npm install just finished. The 10s blank-root default catches // healthy-but-slow renders as defects and fires a useless repair turn. Widen // to 20s for uploads; JS exceptions still fire repair via window.error / // unhandledrejection / dynamic-import .catch. const _uploadType = (import.meta.env.VITE_UPLOAD_TYPE ?? "") as string; const _isUpload = _uploadType === "react_source" || _uploadType === "static_html" || _uploadType === "static_zip"; installErrorObserver(_isUpload ? { blankRootTimeoutMs: 20_000 } : {}); /** * LPS-700 — Guarded bootstrap. * * Why this pattern exists (and why the static `import App ... render()` * single-line bootstrap isn't enough): * * React error boundaries only catch errors thrown DURING RENDER of * already-mounted components. They cannot catch: * * 1. Module-evaluation errors — e.g. a section file with * `React.forwardRef` but missing `import React` throws * `ReferenceError: React is not defined` when the module is * parsed/evaluated, before any component has a chance to mount. * 2. Errors thrown during `createRoot().render()` itself, before * the React tree is installed. * 3. Unhandled promise rejections from dynamic imports. * * With a static `import App`, any of the above leaves `
` * empty with no user-facing feedback. The iframe looks like a blank * white page — the failure mode that triggered LPS-700. * * This guarded bootstrap: * * - Imports App dynamically via `import("./App")` so a throw during * module evaluation lands in `.catch(...)` instead of a top-level * uncaught exception. * - Renders `` as a visible fallback so the user * never sees a blank iframe. * - Posts `APP_BOOT_FAILED` upward so the parent frame can surface a * richer UI or trigger auto-repair. * * `APP_RENDERED` stays posted from inside `App.tsx` (the "the app is * actually visible" signal) and is NOT posted here — we don't want to * lie to the parent about successful render when only the module * resolution succeeded but rendering hasn't happened yet. * * Phase 3 (LPS validation v2-lite+): the boot-window scope of the * window.error / unhandledrejection / MutationObserver listeners is * now owned by `lib/error_observer.ts` which runs for the full * session, not just the boot window. The dynamic-import .catch and * the `APP_BOOT_FAILED` postMessage below stay as a backwards-compat * fallback path so existing PreviewFrame handlers keep working. */ const rootEl = document.getElementById("root"); // Defensive — `#root` is defined in index.html so this should never // happen, but if the boilerplate's index.html were corrupted we don't // want a null-deref masking the real problem. if (!rootEl) { // eslint-disable-next-line no-console console.error("[main] #root element missing from index.html"); } const root: Root | null = rootEl ? createRoot(rootEl) : null; // Track whether we've already reported a boot failure so that duplicate // error events (e.g. window.onerror firing for the same error that a // dynamic-import .catch already handled) don't spam the parent. let bootFailureReported = false; /** * Serialize an arbitrary error-ish value into a plain object that can * cross the postMessage boundary. `structuredClone` doesn't preserve * prototype methods, and `Error` instances lose `.message` / `.stack` * when naively JSON.stringified. */ function serializeError(error: unknown): Record { if (error instanceof Error) { return { name: error.name, message: error.message, stack: error.stack?.slice(0, 4000) ?? "", }; } if (error && typeof error === "object") { try { return { message: String(error), raw: JSON.parse(JSON.stringify(error)) }; } catch { return { message: String(error) }; } } return { message: String(error) }; } /** * Convert Vite/dev-server filenames into workspace-relative paths. * * ErrorEvent.filename usually arrives as a full URL like * `http://localhost:5173/src/App.tsx?t=123`; repair tools need the * stable `src/App.tsx` form. */ function normalizeWorkspaceFilePath(filename: string | undefined): string { if (!filename) return ""; const match = filename.match(/(?:^|\/)(src\/[^\s?:)#]+)/); return match?.[1] ?? ""; } /** * Pull the first workspace file reference out of a stack trace. * * Module-eval failures throw at one specific source line, but Chrome * stacks prefix each frame with the transformed dev-server URL — e.g. * `http://localhost:5173/src/components/sections/Header.tsx?t=...:29:11`. * We want just `src/components/sections/Header.tsx:29:11` so the * agent-side /repair-runtime/ handler has a concrete surgical target. * * Returns `{ file, line, column }` with empty strings / zeros when no * workspace frame is found (e.g. library-internal throws). */ function extractTopFrame( stack: string | undefined ): { file: string; line: number; column: number } { if (!stack) return { file: "", line: 0, column: 0 }; // Strip Vite's ?t=timestamp / ?import cache-busting query params so // downstream consumers see a stable `src/...` path. const frameRegex = /(?:https?:\/\/[^)\s]+?)?\/?(src\/[^\s?)]+?)(?:\?[^:\s)]*)?:(\d+):(\d+)/; const match = stack.match(frameRegex); if (!match) return { file: "", line: 0, column: 0 }; return { file: normalizeWorkspaceFilePath(match[1]), line: parseInt(match[2], 10) || 0, column: parseInt(match[3], 10) || 0, }; } function reportBootFailure( error: unknown, source: string, locationHint?: { file?: string; line?: number; column?: number } ): void { if (bootFailureReported) return; bootFailureReported = true; const serialized = serializeError(error); // Prefer caller-provided location (ErrorEvent has exact filename/ // lineno/colno); fall back to parsing the stack. Either path yields // a concrete `src/...` reference when one exists, which the agent's // /repair-runtime/ handler uses as its surgical target. const stackFrame = extractTopFrame(serialized.stack as string | undefined); const hintFile = locationHint?.file?.trim() ?? ""; const file = hintFile || stackFrame.file; const line = hintFile && locationHint?.line != null && locationHint.line > 0 ? locationHint.line : stackFrame.line; const column = hintFile && locationHint?.column != null && locationHint.column > 0 ? locationHint.column : stackFrame.column; const payload = { ...serialized, source, // "dynamic-import" | "render" | "window.error" | "unhandledrejection" file, // e.g. "src/components/sections/Header.tsx" or "" if unknown line, // 1-indexed line in the source file, 0 when unknown column, // 1-indexed column, 0 when unknown timestamp: Date.now(), }; // eslint-disable-next-line no-console console.error("[main] APP_BOOT_FAILED", payload); // Render the fallback inside the iframe first — if this also throws // the catch below keeps us from looping back into another failure. try { root?.render(); } catch (renderErr) { // eslint-disable-next-line no-console console.error("[main] PreviewBootError itself failed to render", renderErr); } // Notify the parent frame (Next.js PreviewFrame) so it can clear the // loading overlay and surface an actionable state. `window.parent` // is always defined (same window if not framed, so the postMessage // is harmless). try { window.parent.postMessage( { type: "APP_BOOT_FAILED", payload }, "*" ); } catch { // postMessage can throw on exotic cross-origin scenarios — there's // no reasonable recovery so we swallow silently. } } interface ViteErrorPayload { plugin?: string; id?: string; loc?: { file?: string; line?: number; column?: number }; message?: string; } let lastViteError: ViteErrorPayload | null = null; if (typeof import.meta !== "undefined" && import.meta.hot) { import.meta.hot.on( "vite:error", (info: { err?: ViteErrorPayload } | ViteErrorPayload) => { const err = (info as { err?: ViteErrorPayload }).err ?? (info as ViteErrorPayload); if (err) lastViteError = err; } ); } function normalizeViteId(id: string): string { const match = id.match(/(src\/[^\s?)#]+)/); return match ? match[1] : id.replace(/^\/?(?:workspace\/)?/, ""); } async function resolveDynamicImportLocation( importError: unknown ): Promise<{ file: string; line: number; column: number } | undefined> { if (lastViteError) { const file = lastViteError.loc?.file ?? lastViteError.id ?? ""; if (file) { return { file: normalizeViteId(file), line: lastViteError.loc?.line ?? 0, column: lastViteError.loc?.column ?? 0, }; } if (lastViteError.message) { const m = lastViteError.message.match( /from\s+["'](?:\/?)(src\/[^"']+)["']/ ); if (m) return { file: m[1], line: 0, column: 0 }; } } const message = importError instanceof Error ? importError.message : String(importError ?? ""); const stackMatch = importError instanceof Error ? importError.stack?.match(/(src\/[^\s?:)#]+):(\d+):(\d+)/) : null; if (stackMatch) { return { file: stackMatch[1], line: parseInt(stackMatch[2], 10) || 0, column: parseInt(stackMatch[3], 10) || 0, }; } const messageMatch = message.match(/(src\/[^\s?:)#]+):(\d+):(\d+)/); if (messageMatch) { return { file: messageMatch[1], line: parseInt(messageMatch[2], 10) || 0, column: parseInt(messageMatch[3], 10) || 0, }; } return undefined; } import("./App") .then(({ default: App }) => { if (!root) return; try { root.render(); } catch (renderError) { reportBootFailure(renderError, "render"); } }) .catch(async (importError) => { // "Failed to fetch dynamically imported module" is browser-generated for // network-level fetch failures (Vite dev server momentarily unavailable). // Retry once after 2s before declaring a boot failure — covers the case // where a concurrent production build briefly invalidated the module graph // (mpaHtmlGeneratorPlugin add/unlink during multi-page builds). const isTransientFetchError = typeof (importError as { message?: unknown })?.message === "string" && (importError as { message: string }).message.includes( "Failed to fetch dynamically imported module" ); if (isTransientFetchError) { setTimeout(() => { import("./App") .then(({ default: App }) => { // Boot window may have closed during the 2s wait; guard both // conditions so we don't render into a stale root. if (!root || bootComplete) return; try { root.render(); } catch (renderError) { reportBootFailure(renderError, "render"); } }) .catch(async (retryError) => { // Retry also failed — treat as a genuine boot failure. const location = await resolveDynamicImportLocation(retryError); reportBootFailure(retryError, "dynamic-import", location); }); }, 2000); return; } const location = await resolveDynamicImportLocation(importError); reportBootFailure(importError, "dynamic-import", location); });