diff --git a/cli/src/guidance/skills.gen.ts b/cli/src/guidance/skills.gen.ts index 6e2c1d7723..e2cf0be86b 100644 --- a/cli/src/guidance/skills.gen.ts +++ b/cli/src/guidance/skills.gen.ts @@ -5561,6 +5561,8 @@ A raw app has three logical parts: \`index.tsx\` is the bundling entrypoint. It typically renders a top-level \`App\` component. The bundler is esbuild. +**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. + ### Generated bindings (\`wmill.d.ts\` / \`wmill.ts\`) The frontend imports a generated module that mirrors the backend runnables. **Never write to it directly** — it gets regenerated whenever backend runnables change. Modifying it by hand will be overwritten. diff --git a/frontend/scripts/ui_builder_artifact.json b/frontend/scripts/ui_builder_artifact.json index 5346b1076a..b901dc9bc0 100644 --- a/frontend/scripts/ui_builder_artifact.json +++ b/frontend/scripts/ui_builder_artifact.json @@ -1,5 +1,5 @@ { "baseUrl": "https://pub-06154ed168a24e73a86ab84db6bf15d8.r2.dev", - "version": "062d11c", - "sha256": "355bf3acfb935080e4a4fa23d0509e03e4bc01aeed622db110663d7ec6c1c438" + "version": "f8cecf9", + "sha256": "2a79b838e21e06abe06119263872afbe83c4a13f7aa72c722af3e34b819f0ce3" } diff --git a/frontend/src/lib/components/copilot/chat/app/core.ts b/frontend/src/lib/components/copilot/chat/app/core.ts index cbfdcc5616..599a77e906 100644 --- a/frontend/src/lib/components/copilot/chat/app/core.ts +++ b/frontend/src/lib/components/copilot/chat/app/core.ts @@ -927,6 +927,7 @@ export function prepareAppSystemMessage(customPrompt?: string): ChatCompletionSy - The frontend is bundled using esbuild with entrypoint \`index.tsx\` - 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. ### Backend Backend runnables can be of different types: diff --git a/frontend/src/lib/components/raw_apps/RawAppEditor.svelte b/frontend/src/lib/components/raw_apps/RawAppEditor.svelte index d93da9a46c..ba13030d2f 100644 --- a/frontend/src/lib/components/raw_apps/RawAppEditor.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppEditor.svelte @@ -275,6 +275,8 @@ // Latest UI Builder error; cleared on next successful build. let buildError = $state(undefined) + // Latest uncaught runtime error thrown by the rendered app; cleared on next build. + let runtimeError = $state(undefined) let logsCollapsed = $state(false) let logsDiv: HTMLDivElement | undefined = $state(undefined) $effect(() => { @@ -1034,10 +1036,7 @@ // the preview iframe so it renders the new app. if (fromUiBuilder && e.data.type === 'preview') { lastBuild = { css: e.data.css, js: e.data.js } - previewIframe?.contentWindow?.postMessage( - { type: 'preview', css: e.data.css, js: e.data.js }, - '*' - ) + feedPreviewIframe(lastBuild) syncExternalPreview() return } @@ -1067,6 +1066,16 @@ return } + // Uncaught error/rejection from the rendered app — surfaced in the preview + // overlay so a runtime crash isn't a silent blank error. + if (fromPreview && e.data.type === 'runtimeError') { + runtimeError = + typeof e.data.message === 'string' && e.data.message + ? e.data.message + : 'Unknown runtime error' + return + } + // Inspector events come exclusively from the preview iframe. if (fromPreview && e.data.type === 'inspectorSelect') { inspectorElement = e.data.element as InspectorElementInfo @@ -1156,6 +1165,17 @@ } } + // Feed a build into the inline preview iframe. Clears any prior runtime-error + // overlay first: a fresh render supersedes the old crash, and if the new + // render throws again app-preview.html re-posts `runtimeError`. + function feedPreviewIframe(build: { css: string; js: string }) { + runtimeError = undefined + previewIframe?.contentWindow?.postMessage( + { type: 'preview', css: build.css, js: build.js }, + '*' + ) + } + // Full (re)feed of the detached window: theme first, then the build. Used // when (re)attaching to a window — open, focus-reuse, load, handshake — so // it always matches the editor's current state. Plain rebuilds use @@ -1300,10 +1320,7 @@ // Replay the last build so the preview repopulates without // waiting for the user to trigger another bundle. if (lastBuild) { - previewIframe?.contentWindow?.postMessage( - { type: 'preview', css: lastBuild.css, js: lastBuild.js }, - '*' - ) + feedPreviewIframe(lastBuild) } // Escape inside the preview exits inspect mode — the keydown fires in // the iframe's document, so the parent window listener can't see it. @@ -1854,14 +1871,7 @@ aria-label="Rebuild" onclick={() => { if (lastBuild) { - previewIframe?.contentWindow?.postMessage( - { - type: 'preview', - css: lastBuild.css, - js: lastBuild.js - }, - '*' - ) + feedPreviewIframe(lastBuild) } }} > @@ -1911,6 +1921,18 @@ > + {:else if runtimeError} + {/if} {#if logs}