mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-12 00:06:14 +00:00
feat(raw-apps): runtime-error overlay + AI import-React instruction (#9966)
* feat(raw-apps): render runtime-error overlay + instruct AI to import React Render the `runtimeError` message the raw-app preview frame now posts as a prominent overlay, so an uncaught exception that blanks the app is visible instead of silent. Cleared on the next successful build (via a shared `feedPreviewIframe` helper so every preview-feed path resets it). Add an AI app-generation instruction to begin React files with `import React from 'react'`: raw apps bundle with the classic JSX transform, so a missing import compiles fine but throws "React is not defined" at runtime. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(raw-apps): add the import-React rule to the shared raw-app prompt The global AI chat and the raw-app CLI skill draw their raw-app authoring reference from system_prompts/base/raw-app.md — a separate surface from the app chat's inline prompt (core.ts). Add the same "always begin JSX files with `import React`" rule there (esbuild's classic transform needs React in scope, or JSX throws "React is not defined" at runtime) and regenerate the derived prompt files. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(raw-apps): bump ui_builder tarball to f8cecf9 (runtime-error overlay) Pins the ui_builder artifact to windmill-code-ui-builder#15, which pushes uncaught runtime errors from the preview iframe to the parent so the raw-app editor can render them in the error overlay. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
51e1eba1cc
commit
8df613b4d2
@@ -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.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"baseUrl": "https://pub-06154ed168a24e73a86ab84db6bf15d8.r2.dev",
|
||||
"version": "062d11c",
|
||||
"sha256": "355bf3acfb935080e4a4fa23d0509e03e4bc01aeed622db110663d7ec6c1c438"
|
||||
"version": "f8cecf9",
|
||||
"sha256": "2a79b838e21e06abe06119263872afbe83c4a13f7aa72c722af3e34b819f0ce3"
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -275,6 +275,8 @@
|
||||
|
||||
// Latest UI Builder error; cleared on next successful build.
|
||||
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)
|
||||
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 @@
|
||||
>
|
||||
</Alert>
|
||||
</div>
|
||||
{:else if runtimeError}
|
||||
<div class="absolute top-12 left-2 right-2 z-20 isolate" role="alert">
|
||||
<Alert
|
||||
type="error"
|
||||
title="Runtime error"
|
||||
class="relative before:absolute before:inset-0 before:-z-10 before:rounded-md before:bg-surface before:content-['']"
|
||||
>
|
||||
<pre class="overflow-auto whitespace-pre-wrap text-xs max-h-60"
|
||||
>{runtimeError}</pre
|
||||
>
|
||||
</Alert>
|
||||
</div>
|
||||
{/if}
|
||||
{#if logs}
|
||||
<div
|
||||
|
||||
@@ -587,6 +587,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.
|
||||
|
||||
@@ -251,6 +251,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.
|
||||
|
||||
@@ -16,6 +16,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.
|
||||
|
||||
Reference in New Issue
Block a user