Compare commits

...
Author SHA1 Message Date
Guilhem LemouelandClaude Opus 4.8 6512bafad6 feat(raw-apps): clear empty-render hint on renderAppeared retraction
Handle the preview's `renderAppeared` message (posted when a slow app
mounts #root after the empty-render grace window) by clearing the
"Nothing rendered" hint, so a legitimate late render doesn't leave a
false-positive overlay.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 14:09:23 +02:00
Guilhem LemouelandClaude Opus 4.8 3edd3e65ec feat(raw-apps): show hint when a build renders nothing into #root
Mirror the runtime-error overlay for the silent blank-screen case: when the
preview reports `emptyRender` (build ran cleanly but never mounted #root),
show an info Alert pointing at the missing createRoot(...).render(...) call.

The handler is dormant-safe until the builder tarball that emits `emptyRender`
is pinned, so it can land ahead of the artifact bump.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 13:18:04 +02:00
Guilhem LemouelandClaude Opus 4.8 aef4db4117 docs: clarify raw-app index.tsx must mount App to avoid blank screen
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 13:18:04 +02:00
6 changed files with 105 additions and 4 deletions
+15 -1
View File
@@ -5585,7 +5585,21 @@ A raw app has three logical parts:
### Entrypoint
\`index.tsx\` is the bundling entrypoint. It typically renders a top-level \`App\` component. The bundler is esbuild.
\`index.tsx\` is the bundling **and mount** entrypoint (esbuild bundles it; the preview then executes it against an empty \`<div id="root">\`). It MUST itself mount a top-level \`App\` component into \`#root\` — nothing is auto-rendered for you. Keep your UI in \`App.tsx\` (or \`App.svelte\` / \`App.vue\`) and keep \`index.tsx\` as the mount shim.
React:
\`\`\`tsx
import React from 'react'
import { createRoot } from 'react-dom/client'
import App from './App'
createRoot(document.getElementById('root')!).render(<App />)
\`\`\`
Svelte: \`mount(App, { target: document.getElementById('root')! })\`. Vue: \`createApp(App).mount('#root')\`.
**Never turn \`index.tsx\` into a bare component** (e.g. \`export default function App() { ... }\` with no \`createRoot(...).render(...)\`). A component that is defined but never mounted renders **a blank screen with NO error thrown** — the JSX never executes, so nothing surfaces in the console or the error overlay. If the app is blank, the first thing to check is that \`index.tsx\` actually calls \`createRoot(document.getElementById('root')!).render(<App />)\`.
**Always begin every React file (\`.tsx\`/\`.jsx\`) that uses JSX with \`import React from 'react'\`.** esbuild uses the classic JSX transform, so \`React\` must be in scope wherever JSX appears — a missing import compiles fine but throws \`React is not defined\` at runtime, leaving a blank screen.
@@ -925,6 +925,16 @@ export function prepareAppSystemMessage(customPrompt?: string): ChatCompletionSy
### Frontend
- The frontend is bundled using esbuild with entrypoint \`index.tsx\`
- \`index.tsx\` is also the **mount** entrypoint: it MUST mount a top-level \`App\` into \`#root\` — nothing is auto-rendered. Keep your UI in \`App.tsx\` and keep \`index.tsx\` as the mount shim:
\`\`\`tsx
import React from 'react'
import { createRoot } from 'react-dom/client'
import App from './App'
createRoot(document.getElementById('root')!).render(<App />)
\`\`\`
(Svelte: \`mount(App, { target: document.getElementById('root')! })\`; Vue: \`createApp(App).mount('#root')\`.)
- **Never replace \`index.tsx\` with a bare component.** A component that is defined but never mounted produces a **blank screen with NO error** (the JSX never runs, so nothing throws). If the app renders blank, first verify \`index.tsx\` calls \`createRoot(document.getElementById('root')!).render(<App />)\`.
- Frontend files are managed separately from backend runnables
- The \`wmill.d.ts\` file is generated automatically from the backend runnables shape
- Begin every React file (\`.tsx\`/\`.jsx\`) that uses JSX with \`import React from 'react'\`. Raw apps bundle with the classic JSX transform, so \`React\` must be in scope wherever JSX is used — a missing import compiles fine but throws \`React is not defined\` at runtime.
@@ -282,6 +282,10 @@
let buildError = $state<string | undefined>(undefined)
// Latest uncaught runtime error thrown by the rendered app; cleared on next build.
let runtimeError = $state<string | undefined>(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,19 @@
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
}
// A slow app (async fetch, Suspense) mounted #root after the empty-render
// grace window, so the preview retracts the hint to avoid a false positive.
if (fromPreview && e.data.type === 'renderAppeared') {
emptyRender = false
return
}
// Inspector events come exclusively from the preview iframe.
if (fromPreview && e.data.type === 'inspectorSelect') {
inspectorElement = e.data.element as InspectorElementInfo
@@ -1193,6 +1210,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 +1977,23 @@
>
</Alert>
</div>
{:else if emptyRender}
<div class="absolute top-12 left-2 right-2 z-20 isolate" role="status">
<Alert
type="info"
title="Nothing rendered"
class="relative before:absolute before:inset-0 before:-z-10 before:rounded-md before:bg-surface before:content-['']"
>
<span class="text-xs">
The build succeeded but nothing was mounted into <code>#root</code>. Make
sure your entrypoint (<code>index.tsx</code>) calls
<code
>createRoot(document.getElementById('root')!).render(&lt;App /&gt;)</code
>
rather than only defining a component.
</span>
</Alert>
</div>
{/if}
{#if logs}
<div
+15 -1
View File
@@ -611,7 +611,21 @@ A raw app has three logical parts:
### Entrypoint
\`index.tsx\` is the bundling entrypoint. It typically renders a top-level \`App\` component. The bundler is esbuild.
\`index.tsx\` is the bundling **and mount** entrypoint (esbuild bundles it; the preview then executes it against an empty \`<div id="root">\`). It MUST itself mount a top-level \`App\` component into \`#root\` — nothing is auto-rendered for you. Keep your UI in \`App.tsx\` (or \`App.svelte\` / \`App.vue\`) and keep \`index.tsx\` as the mount shim.
React:
\`\`\`tsx
import React from 'react'
import { createRoot } from 'react-dom/client'
import App from './App'
createRoot(document.getElementById('root')!).render(<App />)
\`\`\`
Svelte: \`mount(App, { target: document.getElementById('root')! })\`. Vue: \`createApp(App).mount('#root')\`.
**Never turn \`index.tsx\` into a bare component** (e.g. \`export default function App() { ... }\` with no \`createRoot(...).render(...)\`). A component that is defined but never mounted renders **a blank screen with NO error thrown** — the JSX never executes, so nothing surfaces in the console or the error overlay. If the app is blank, the first thing to check is that \`index.tsx\` actually calls \`createRoot(document.getElementById('root')!).render(<App />)\`.
**Always begin every React file (\`.tsx\`/\`.jsx\`) that uses JSX with \`import React from 'react'\`.** esbuild uses the classic JSX transform, so \`React\` must be in scope wherever JSX appears — a missing import compiles fine but throws \`React is not defined\` at runtime, leaving a blank screen.
@@ -249,7 +249,21 @@ A raw app has three logical parts:
### Entrypoint
`index.tsx` is the bundling entrypoint. It typically renders a top-level `App` component. The bundler is esbuild.
`index.tsx` is the bundling **and mount** entrypoint (esbuild bundles it; the preview then executes it against an empty `<div id="root">`). It MUST itself mount a top-level `App` component into `#root` — nothing is auto-rendered for you. Keep your UI in `App.tsx` (or `App.svelte` / `App.vue`) and keep `index.tsx` as the mount shim.
React:
```tsx
import React from 'react'
import { createRoot } from 'react-dom/client'
import App from './App'
createRoot(document.getElementById('root')!).render(<App />)
```
Svelte: `mount(App, { target: document.getElementById('root')! })`. Vue: `createApp(App).mount('#root')`.
**Never turn `index.tsx` into a bare component** (e.g. `export default function App() { ... }` with no `createRoot(...).render(...)`). A component that is defined but never mounted renders **a blank screen with NO error thrown** — the JSX never executes, so nothing surfaces in the console or the error overlay. If the app is blank, the first thing to check is that `index.tsx` actually calls `createRoot(document.getElementById('root')!).render(<App />)`.
**Always begin every React file (`.tsx`/`.jsx`) that uses JSX with `import React from 'react'`.** esbuild uses the classic JSX transform, so `React` must be in scope wherever JSX appears — a missing import compiles fine but throws `React is not defined` at runtime, leaving a blank screen.
+15 -1
View File
@@ -14,7 +14,21 @@ A raw app has three logical parts:
### Entrypoint
`index.tsx` is the bundling entrypoint. It typically renders a top-level `App` component. The bundler is esbuild.
`index.tsx` is the bundling **and mount** entrypoint (esbuild bundles it; the preview then executes it against an empty `<div id="root">`). It MUST itself mount a top-level `App` component into `#root` — nothing is auto-rendered for you. Keep your UI in `App.tsx` (or `App.svelte` / `App.vue`) and keep `index.tsx` as the mount shim.
React:
```tsx
import React from 'react'
import { createRoot } from 'react-dom/client'
import App from './App'
createRoot(document.getElementById('root')!).render(<App />)
```
Svelte: `mount(App, { target: document.getElementById('root')! })`. Vue: `createApp(App).mount('#root')`.
**Never turn `index.tsx` into a bare component** (e.g. `export default function App() { ... }` with no `createRoot(...).render(...)`). A component that is defined but never mounted renders **a blank screen with NO error thrown** — the JSX never executes, so nothing surfaces in the console or the error overlay. If the app is blank, the first thing to check is that `index.tsx` actually calls `createRoot(document.getElementById('root')!).render(<App />)`.
**Always begin every React file (`.tsx`/`.jsx`) that uses JSX with `import React from 'react'`.** esbuild uses the classic JSX transform, so `React` must be in scope wherever JSX appears — a missing import compiles fine but throws `React is not defined` at runtime, leaving a blank screen.