diff --git a/frontend/src/lib/components/raw_apps/RawAppEditor.svelte b/frontend/src/lib/components/raw_apps/RawAppEditor.svelte index 9e6f508b59..ff345db666 100644 --- a/frontend/src/lib/components/raw_apps/RawAppEditor.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppEditor.svelte @@ -282,6 +282,10 @@ let buildError = $state(undefined) // Latest uncaught runtime error thrown by the rendered app; cleared on next build. let runtimeError = $state(undefined) + // True when a build ran cleanly but never mounted anything into #root (e.g. the + // entrypoint defines a component but never calls createRoot(...).render(...)). + // Cleared on next build. + let emptyRender = $state(false) let logsCollapsed = $state(false) let logsDiv: HTMLDivElement | undefined = $state(undefined) $effect(() => { @@ -1099,6 +1103,13 @@ return } + // Build ran but #root stayed empty — the entrypoint likely never mounted + // the app. Surfaced as a hint so a silent blank screen is self-explanatory. + if (fromPreview && e.data.type === 'emptyRender') { + emptyRender = true + return + } + // Inspector events come exclusively from the preview iframe. if (fromPreview && e.data.type === 'inspectorSelect') { inspectorElement = e.data.element as InspectorElementInfo @@ -1193,6 +1204,7 @@ // render throws again app-preview.html re-posts `runtimeError`. function feedPreviewIframe(build: { css: string; js: string }) { runtimeError = undefined + emptyRender = false previewIframe?.contentWindow?.postMessage( { type: 'preview', css: build.css, js: build.js }, '*' @@ -1959,6 +1971,23 @@ > + {:else if emptyRender} +
+ + + The build succeeded but nothing was mounted into #root. Make + sure your entrypoint (index.tsx) calls + createRoot(document.getElementById('root')!).render(<App />) + rather than only defining a component. + + +
{/if} {#if logs}