import { Component, lazy, Suspense, useEffect, type ErrorInfo, type ReactNode } from 'react' import { Routes, Route, Navigate } from 'react-router-dom' import { ReactLenis } from 'lenis/react' import { routes } from './routes' import Layout from './components/Layout' const pages = import.meta.glob('./pages/*.tsx') function getPageComponent(pageFile: string) { const key = `./pages/${pageFile}.tsx` const loader = pages[key] if (!loader) return null return lazy(loader as () => Promise<{ default: React.ComponentType }>) } // ─── Render-state probe ──────────────────────────────────────────────────── // After every HMR (and on first mount) the iframe reports back to a Vite // dev-only middleware whether the page actually rendered. Bob-AI reads the // resulting file from inside the sandbox after each commit and uses it to // detect "blank preview" / "React threw on mount" failures that escape the // pre-commit gate (esbuild + tsc see only static errors). Sandbox-only — // public deploys never iframe themselves so the probe stays silent. const RENDER_STATUS_URL = '/__webild/render-status' const RENDER_PROBE_DELAY_MS = 1500 const RENDER_PROBE_MIN_TEXT_LEN = 30 interface RenderStatusPayload { ok: boolean reason?: string error?: string stack?: string componentStack?: string filename?: string lineno?: number colno?: number rootChildren?: number bodyTextLen?: number } function reportRenderStatus(payload: RenderStatusPayload) { if (typeof window === 'undefined') return if (window.parent === window) return try { fetch(RENDER_STATUS_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...payload, t: Date.now() }), keepalive: true, }).catch(() => {}) } catch { // ignore } } function probeRenderState() { if (typeof document === 'undefined') return const root = document.getElementById('root') const text = (document.body?.innerText || '').trim() if (!root || root.children.length === 0) { reportRenderStatus({ ok: false, reason: 'empty_root', rootChildren: 0, bodyTextLen: text.length, }) return } if (text.length < RENDER_PROBE_MIN_TEXT_LEN) { reportRenderStatus({ ok: false, reason: 'blank_render', rootChildren: root.children.length, bodyTextLen: text.length, }) return } reportRenderStatus({ ok: true, rootChildren: root.children.length, bodyTextLen: text.length, }) } declare global { interface Window { __webildRenderProbeInstalled__?: boolean } } if (typeof window !== 'undefined' && window.parent !== window && !window.__webildRenderProbeInstalled__) { window.__webildRenderProbeInstalled__ = true window.addEventListener('error', (e) => { reportRenderStatus({ ok: false, reason: 'window_error', error: String(e?.message || (e as ErrorEvent)?.error?.message || 'unknown'), stack: String((e as ErrorEvent)?.error?.stack || '').slice(0, 4000), filename: String((e as ErrorEvent)?.filename || ''), lineno: (e as ErrorEvent)?.lineno, colno: (e as ErrorEvent)?.colno, }) }) window.addEventListener('unhandledrejection', (e) => { const reason = (e as PromiseRejectionEvent).reason reportRenderStatus({ ok: false, reason: 'unhandled_rejection', error: String(reason?.message || reason || 'unknown'), stack: String(reason?.stack || '').slice(0, 4000), }) }) if (import.meta.hot) { import.meta.hot.on('vite:afterUpdate', () => { setTimeout(probeRenderState, RENDER_PROBE_DELAY_MS) }) } } class RenderErrorBoundary extends Component<{ children: ReactNode }, { hasError: boolean }> { state = { hasError: false } static getDerivedStateFromError() { return { hasError: true } } componentDidCatch(error: Error, info: ErrorInfo) { reportRenderStatus({ ok: false, reason: 'react_error_boundary', error: String(error?.message || error || 'unknown'), stack: String(error?.stack || '').slice(0, 4000), componentStack: String(info?.componentStack || '').slice(0, 4000), }) } render() { return this.state.hasError ? null : this.props.children } } function useRenderProbe() { useEffect(() => { const id = setTimeout(probeRenderState, RENDER_PROBE_DELAY_MS) return () => clearTimeout(id) }, []) } // Selection bridge: Alt+click on a `[data-webild-section]` block posts the // section name to the parent (Webild editor) so the next /edit request can // be scoped surgically. Sandbox-only — the Webild editor is the parent; // public visitors hitting the deployed site never trigger this. function useWebildSelectionBridge() { useEffect(() => { if (typeof window === 'undefined') return if (window.parent === window) return const handler = (e: MouseEvent) => { if (!e.altKey) return let el = e.target as HTMLElement | null while (el && el !== document.body) { const name = el.getAttribute?.('data-webild-section') if (name) { e.preventDefault() window.parent.postMessage( { type: 'webild:section-click', sectionName: name }, '*', ) return } el = el.parentElement } } window.addEventListener('click', handler, true) return () => window.removeEventListener('click', handler, true) }, []) } function App() { useWebildSelectionBridge() useRenderProbe() return (
} > }> {routes.map((route) => { const Page = getPageComponent(route.pageFile) if (!Page) return null return } /> })} } />
) } export default App