refactor: remove vue as a full-code app framework (#11690)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
AlexRV12
2026-10-12 12:31:01 +02:00
committed by GitHub
co-authored by Claude Opus 5.5
parent a8e2f1f1ca
commit 54eeb451e4
17 changed files with 52 additions and 505 deletions
+1 -1
View File
@@ -6,7 +6,7 @@
* The build itself is `wmill app bundle`, the same one `wmill app push` runs, so
* an app deployed through the API is compiled exactly as the CLI and the editor
* compile it — entry point, virtual `wmill` module, `/ui/` shared-UI resolution
* and the Svelte/Vue plugins included. Reimplementing any of that here would be
* and the Svelte plugin included. Reimplementing any of that here would be
* a third bundler to keep in step with the other two.
*/
declare const __wmillAppPolicy: {
+5 -17
View File
@@ -58,10 +58,10 @@ function conditionsFor(svelte: boolean): string[] {
/**
* Detects which frontend frameworks are present in package.json
*/
export function detectFrameworks(appDir: string): { svelte: boolean; vue: boolean } {
export function detectFrameworks(appDir: string): { svelte: boolean } {
const packageJsonPath = path.join(appDir, "package.json");
if (!fs.existsSync(packageJsonPath)) {
return { svelte: false, vue: false };
return { svelte: false };
}
try {
@@ -73,10 +73,9 @@ export function detectFrameworks(appDir: string): { svelte: boolean; vue: boolea
return {
svelte: "svelte" in allDeps,
vue: "vue" in allDeps,
};
} catch {
return { svelte: false, vue: false };
return { svelte: false };
}
}
@@ -256,17 +255,6 @@ export async function createFrameworkPlugins(appDir: string): Promise<any[]> {
plugins.push(createSveltePlugin(appDir));
}
if (frameworks.vue) {
log.info(colors.blue("🔧 Vue detected, adding vue plugin..."));
throw new Error("Vue plugin not supported yet");
// try {
// const esbuildPluginVue = await import("esbuild-plugin-vue3");
// plugins.push(esbuildPluginVue.default());
// } catch (error: any) {
// log.warn(colors.yellow(`Failed to load vue plugin: ${error.message}`));
// }
}
return plugins;
}
@@ -313,7 +301,7 @@ export async function createBundle(
// Use the entryPoint's directory if provided, otherwise fall back to cwd.
const appDir = options.entryPoint ? path.dirname(options.entryPoint) : process.cwd();
const frameworks = detectFrameworks(appDir);
const defaultEntry = (frameworks.svelte || frameworks.vue) ? "index.ts" : "index.tsx";
const defaultEntry = frameworks.svelte ? "index.ts" : "index.tsx";
const entryPoint = options.entryPoint ?? defaultEntry;
const outDir = options.outDir ?? "dist";
@@ -331,7 +319,7 @@ export async function createBundle(
// Ensure node_modules exists in the app directory
await ensureNodeModules(appDir);
// Load framework-specific plugins (svelte, vue) based on package.json
// Load framework-specific plugins (svelte) based on package.json
const frameworkPlugins = await createFrameworkPlugins(appDir);
// Ensure output directory exists
+2 -2
View File
@@ -12,10 +12,10 @@ interface BundleOptions {
}
/** Every entry point a raw app may have, most specific first, with the
* framework's preferred one promoted — Svelte and Vue mount from a `.ts`. */
* framework's preferred one promoted — Svelte mounts from a `.ts`. */
function entryPointFor(appDir: string): string {
const frameworks = detectFrameworks(appDir);
const candidates = frameworks.svelte || frameworks.vue
const candidates = frameworks.svelte
? ["index.ts", "index.tsx", "index.js"]
: ["index.tsx", "index.ts", "index.js"];
const entry = candidates.find((c) => fs.existsSync(path.join(appDir, c)));
+3 -3
View File
@@ -481,7 +481,7 @@ async function dev(opts: DevOptions, appFolder?: string) {
// Detect frameworks to determine default entry point
const frameworks = detectFrameworks(process.cwd());
const defaultEntry = frameworks.svelte || frameworks.vue
const defaultEntry = frameworks.svelte
? "index.ts"
: "index.tsx";
const entryPoint = opts.entry ?? defaultEntry;
@@ -577,7 +577,7 @@ async function dev(opts: DevOptions, appFolder?: string) {
const buildOptions = getDevBuildOptions(entryPoint, frameworks.svelte);
// Load framework-specific plugins (svelte, vue) based on package.json
// Load framework-specific plugins (svelte) based on package.json
const frameworkPlugins = await createFrameworkPlugins(appDir);
const wmillPlugin = {
@@ -1602,7 +1602,7 @@ const command = new Command()
})
.option(
"--entry <entry:string>",
"Entry point file (default: index.ts for Svelte/Vue, index.tsx otherwise)",
"Entry point file (default: index.ts for Svelte, index.tsx otherwise)",
)
.option("--no-open", "Don't automatically open the browser")
.option(
+1 -1
View File
@@ -114,7 +114,7 @@ async function validateBuild(
// Try to create a bundle - this will validate that all dependencies are in place
const frameworks = detectFrameworks(appDir);
const entryFile = frameworks.svelte || frameworks.vue ? "index.ts" : "index.tsx";
const entryFile = frameworks.svelte ? "index.ts" : "index.tsx";
const entryPoint = path.join(appDir, entryFile);
await createBundle({
entryPoint,
+3 -34
View File
@@ -85,21 +85,6 @@ const app = mount(App, { target: document.getElementById("root")! });
export default app;
`;
const appVue = `<template>
<h1>Hello {{ msg }}</h1>
</template>
<script setup>
import { ref } from 'vue';
const msg = ref('world');
</script>`;
const indexVue = `import { createApp } from 'vue'
import App from './App.vue'
import "./index.css";
createApp(App).mount('#root')`;
const indexCss = `body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
@@ -171,21 +156,6 @@ const templates: Record<string, FrameworkTemplate> = {
"svelte": "^5.55.5",
"windmill-client": "^1"
}
}`,
},
},
vue: {
name: "Vue 3",
files: {
"/index.ts": indexVue,
"/App.vue": appVue,
"/index.css": indexCss,
"/package.json": `{
"dependencies": {
"core-js": "3.26.1",
"vue": "3.5.13",
"windmill-client": "^1"
}
}`,
},
},
@@ -251,7 +221,7 @@ interface DataConfig {
schema?: string;
}
type FrameworkKey = "react19" | "react18" | "svelte5" | "vue";
type FrameworkKey = "react19" | "react18" | "svelte5";
interface NewAppOptions extends GlobalOptions {
/** App summary (short description). Skips the prompt when provided. */
@@ -384,7 +354,7 @@ async function newApp(opts: NewAppOptions) {
}
// Ask for framework (skipped if --framework is provided)
const VALID_FRAMEWORKS: FrameworkKey[] = ["react19", "react18", "svelte5", "vue"];
const VALID_FRAMEWORKS: FrameworkKey[] = ["react19", "react18", "svelte5"];
let framework: string;
if (opts.framework !== undefined) {
if (!VALID_FRAMEWORKS.includes(opts.framework)) {
@@ -403,7 +373,6 @@ async function newApp(opts: NewAppOptions) {
{ name: "React 19 (Recommended)", value: "react19" },
{ name: "React 18", value: "react18" },
{ name: "Svelte 5", value: "svelte5" },
{ name: "Vue 3", value: "vue" },
],
});
}
@@ -900,7 +869,7 @@ const command = new Command()
)
.option(
"--framework <framework:string>",
"Framework template: react19 | react18 | svelte5 | vue. Skips the prompt when provided. Triggers non-interactive mode."
"Framework template: react19 | react18 | svelte5. Skips the prompt when provided. Triggers non-interactive mode."
)
.option(
"--datatable <datatable:string>",
+1 -1
View File
@@ -461,7 +461,7 @@ export async function pushRawApp(
log.info(colors.yellow.bold(`Creating raw app ${remotePath} bundle...`));
// Detect frameworks to determine entry point
const frameworks = detectFrameworks(localPath);
const entryFile = frameworks.svelte || frameworks.vue
const entryFile = frameworks.svelte
? "index.ts"
: "index.tsx";
const entryPoint = localPath + entryFile;
+8 -8
View File
@@ -6351,7 +6351,7 @@ You need three values to run the command:
1. **summary** — a short description of the app
2. **path** — the windmill path, e.g. \`f/folder/my_app\` or \`u/username/my_app\`
3. **framework** — one of \`react19\` (recommended), \`react18\`, \`svelte5\`, \`vue\`
3. **framework** — one of \`react19\` (recommended), \`react18\`, \`svelte5\`
**summary and path**: if the user's request did not supply them, ask. Do not guess values and do not invent paths.
@@ -6423,7 +6423,7 @@ my_app{{RAW_APP_SUFFIX}}/
├── DATATABLES.md # Database schemas (run 'wmill app generate-agents' to refresh)
├── raw_app.yaml # App configuration (summary, path, data settings)
├── index.tsx # Frontend entry point
├── App.tsx # Main React/Svelte/Vue component
├── App.tsx # Main React/Svelte component
├── index.css # Styles
├── package.json # Frontend dependencies
├── wmill.ts # Auto-generated backend type definitions (DO NOT EDIT)
@@ -6581,7 +6581,7 @@ For the rest, tell the user which command fits their intent and let them run it
# Windmill Raw Apps
Raw apps let you build custom frontends with React, Svelte, or Vue that connect to Windmill backend runnables and datatables.
Raw apps let you build custom frontends with React or Svelte that connect to Windmill backend runnables and datatables.
## App shape
@@ -6597,7 +6597,7 @@ A new app gets the setup Windmill's own new-app dialog gives one: a framework, t
### Framework
Use the framework the user named. When they named none, follow what there is to follow: the app being edited, or the apps the request points at. With nothing to follow, use **React 19**, the recommended default, **without asking**. React 18, Svelte 5 and Vue are for when they are asked for.
Use the framework the user named. When they named none, follow what there is to follow: the app being edited, or the apps the request points at. With nothing to follow, use **React 19**, the recommended default, **without asking**. React 18 and Svelte 5 are for when they are asked for. These three are the only frameworks the raw app bundler builds. When the user asks to build or convert an app in another framework, write no code in it: say it is not available and offer these three instead.
### Data setup
@@ -6627,7 +6627,7 @@ Then build what was chosen: the data table and schema become the app's \`data.da
### Entrypoint
The entrypoint is \`index.tsx\` for React and \`index.ts\` for Svelte and Vue. It is both the bundling entrypoint (the bundler is esbuild) and the **mount** entrypoint: the preview executes the bundle against an empty \`<div id="root">\` and auto-renders nothing, so the entrypoint must mount a top-level \`App\` itself. Keep the UI in \`App.tsx\` / \`App.svelte\` / \`App.vue\` and keep the entrypoint as the mount shim.
The entrypoint is \`index.tsx\` for React and \`index.ts\` for Svelte. It is both the bundling entrypoint (the bundler is esbuild) and the **mount** entrypoint: the preview executes the bundle against an empty \`<div id="root">\` and auto-renders nothing, so the entrypoint must mount a top-level \`App\` itself. Keep the UI in \`App.tsx\` / \`App.svelte\` and keep the entrypoint as the mount shim.
React (\`index.tsx\`):
@@ -6639,7 +6639,7 @@ import App from './App'
createRoot(document.getElementById('root')!).render(<App />)
\`\`\`
Svelte (\`index.ts\`): \`mount(App, { target: document.getElementById('root')! })\`. Vue (\`index.ts\`): \`createApp(App).mount('#root')\`.
Svelte (\`index.ts\`): \`mount(App, { target: document.getElementById('root')! })\`.
**Never replace the entrypoint with a bare component** (\`export default function App() { ... }\` and no mount call). A component that is defined but never mounted renders a blank screen with **no error thrown** — it never executes, so nothing reaches the console or the error overlay. If an app renders blank, check that the entrypoint still mounts \`App\` into \`#root\`.
@@ -8102,7 +8102,7 @@ app related commands
- \`app dev [app_folder:string]\` - Start a development server for building apps with live reload and hot module replacement
- \`--port <port:number>\` - Port to run the dev server on (will find next available port if occupied)
- \`--host <host:string>\` - Host to bind the dev server to
- \`--entry <entry:string>\` - Entry point file (default: index.ts for Svelte/Vue, index.tsx otherwise)
- \`--entry <entry:string>\` - Entry point file (default: index.ts for Svelte, index.tsx otherwise)
- \`--no-open\` - Don't automatically open the browser
- \`--recording\` - Frame the app in a shell with a Record button, to capture a replayable session recording of the app under development
- \`app lint [app_folder:string]\` - Lint a raw app folder to validate structure and buildability
@@ -8113,7 +8113,7 @@ app related commands
- \`app new\` - create a new raw app from a template
- \`--summary <summary:string>\` - App summary (short description). Skips the prompt when provided. Triggers non-interactive mode.
- \`--path <path:string>\` - App path (e.g., f/folder/my_app or u/username/my_app). Skips the prompt when provided. Triggers non-interactive mode.
- \`--framework <framework:string>\` - Framework template: react19 | react18 | svelte5 | vue. Skips the prompt when provided. Triggers non-interactive mode.
- \`--framework <framework:string>\` - Framework template: react19 | react18 | svelte5. Skips the prompt when provided. Triggers non-interactive mode.
- \`--datatable <datatable:string>\` - Datatable to wire up. Without this flag in non-interactive mode, no datatable is configured.
- \`--schema <schema:string>\` - Schema to use with --datatable. If it doesn't exist yet, a sql_to_apply/ migration (CREATE SCHEMA IF NOT EXISTS) is written for it.
- \`--overwrite\` - Overwrite the target directory if it already exists, without prompting.
@@ -1306,12 +1306,7 @@ const readDomSchema = z.object({
const takeScreenshotSchema = z.object({})
const FRAMEWORK_KEYS = [
'react19',
'react18',
'svelte5',
'vue'
] as const satisfies readonly FrameworkKey[]
const FRAMEWORK_KEYS = ['react19', 'react18', 'svelte5'] as const satisfies readonly FrameworkKey[]
const initAppSchema = z.object({
path: z
@@ -2493,7 +2488,7 @@ function getAppInstructions(language?: ScriptLang): string {
- Global mode edits raw app drafts only; it does not save or deploy unless the user explicitly asks to deploy.
- App drafts are addressed by workspace path. Follow the path conventions in the system prompt: default to \`u/<current-user>/<name>\` for bare names; only use \`f/<folder>/<name>\` when the folder is known to exist. The first write tool snapshots the workspace app onto the draft, and subsequent writes accumulate.
- To create a new app, use \`init_app\` with a path, optional summary, a framework (\`react19\` / \`react18\` / \`svelte5\` / \`vue\`) and, for an app that stores data, its \`data\` setup. "Starting a new app" below decides the framework and when the data setup is asked. Ask each of its questions with \`askUserQuestion\`, one question per call with the suggested answer first, before \`init_app\` and before any SQL. \`init_app\` only records \`data\`: a new schema is yours to create with the datatable SQL tool. \`init_app\` errors if an app already exists at the path or a draft is already in flight; in that case, edit the existing one rather than re-initializing.
- To create a new app, use \`init_app\` with a path, optional summary, a framework (\`react19\` / \`react18\` / \`svelte5\`) and, for an app that stores data, its \`data\` setup. "Starting a new app" below decides the framework and when the data setup is asked. Ask each of its questions with \`askUserQuestion\`, one question per call with the suggested answer first, before \`init_app\` and before any SQL. \`init_app\` only records \`data\`: a new schema is yours to create with the datatable SQL tool. \`init_app\` errors if an app already exists at the path or a draft is already in flight; in that case, edit the existing one rather than re-initializing.
- \`init_app\` seeds a starter inline runnable named \`a\` (bun, \`main(x: string) => string\`) so the React/Svelte demo button works on first render. Replace or remove it once you start building real backend runnables.
- Frontend file paths start with \`/\` (e.g. \`/index.tsx\`, \`/App.tsx\`, \`/styles.css\`). Use \`write_app_file\` / \`patch_app_file\` / \`delete_app_file\`.
- Backend inline runnables are addressed as \`backend/<key>/main.{ts|py}\` from the file tools, but you create or update them via \`write_app_runnable\` / \`delete_app_runnable\` (which take the runnable shape directly: \`{ name, type, inlineScript?, path?, staticInputs? }\`).
@@ -496,7 +496,7 @@ addEventListener('message', function (e) {
// Set when a build ran cleanly but never mounted anything into #root — the
// entrypoint defines a component without ever mounting it. Cleared on next build.
let emptyRender = $state(false)
// The repair hint above has to match the app's framework: only React apps have
// The repair hint above has to match the app's framework: only React apps have
// an `index.tsx` and `createRoot`; Svelte mounts from `index.ts`. Keyed
// off file extensions rather than exact template filenames, which users rename.
let mountHint = $derived.by(() => {
@@ -507,9 +507,6 @@ addEventListener('message', function (e) {
entrypoint: entrypoint ?? 'index.ts',
call: "mount(App, { target: document.getElementById('root')! })"
}
}
if (paths.some((p) => p.endsWith('.vue'))) {
return { entrypoint: entrypoint ?? 'index.ts', call: "createApp(App).mount('#root')" }
}
return {
entrypoint: entrypoint ?? 'index.tsx',
@@ -1,5 +1,5 @@
// The starter app is a tour of what a raw app can do: each card calls one of the
// STARTER_RUNNABLES below through a different `./wmill` export. The three framework
// STARTER_RUNNABLES below through a different `./wmill` export. The framework
// variants share `indexCss` and must stay in step with each other and with the runnables.
const reactIndex = `
@@ -13,7 +13,7 @@ root.render(<App/>);
`
const STATS_SAMPLE_TEXT =
'Windmill turns scripts into apps. Write the frontend in React, Svelte or Vue, and the backend in TypeScript, Python or any other supported language. Scripts run on workers, and the app calls them like functions.'
'Windmill turns scripts into apps. Write the frontend in React or Svelte, and the backend in TypeScript, Python or any other supported language. Scripts run on workers, and the app calls them like functions.'
const appTsx = `import React, { useState } from 'react'
import { backend, backendAsync, getJob, streamJob } from './wmill'
@@ -516,230 +516,6 @@ const app = mount(App, { target: document.getElementById("root")! });
export default app;
`
const appVue = `<template>
<main class="app">
<header class="hero">
<span class="eyebrow">Windmill app{{ ctx.workspace ? ' · ' + ctx.workspace : '' }}</span>
<h1>{{ firstName ? 'Welcome, ' + firstName : 'Welcome' }}</h1>
<p class="muted">
A Vue frontend wired to backend runnables. Each card shows one thing an app can do.
Edit App.vue and the runnables to make it yours, or ask the AI to build on it.
</p>
</header>
<div class="grid">
<section class="card">
<div class="card-header">
<span class="card-step">1</span>
<div>
<h2>Call a backend runnable</h2>
<p class="muted">Runnables are scripts that run on Windmill workers. Call them like async functions; context fields add who is calling.</p>
</div>
</div>
<div class="card-body">
<form class="row" @submit.prevent="greet">
<input v-model="name" placeholder="Your name" />
<button type="submit" :disabled="greetLoading">{{ greetLoading ? 'Running...' : 'Run' }}</button>
</form>
<div class="output">
<span v-if="greetError" class="error">{{ greetError }}</span>
<template v-else-if="greeting">
<strong>{{ greeting.message }}</strong>
<span class="muted"> Ran at {{ new Date(greeting.ran_at).toLocaleTimeString() }}</span>
</template>
<span v-else class="muted">The result shows up here.</span>
</div>
</div>
<pre class="snippet"><code>const res = await backend.greet({ name })</code></pre>
</section>
<section class="card">
<div class="card-header">
<span class="card-step">2</span>
<div>
<h2>Use any language</h2>
<p class="muted">Each runnable picks its own language. This one is Python, called the same way.</p>
</div>
</div>
<div class="card-body">
<textarea rows="4" v-model="text"></textarea>
<div class="row">
<button @click="analyze" :disabled="statsLoading">
{{ statsLoading ? 'Analyzing...' : 'Analyze with Python' }}
</button>
</div>
<div v-if="statsError" class="output error">{{ statsError }}</div>
<div v-if="stats" class="stats">
<div><strong>{{ stats.words }}</strong><span class="muted">words</span></div>
<div><strong>{{ stats.sentences }}</strong><span class="muted">sentences</span></div>
<div><strong>{{ stats.characters }}</strong><span class="muted">characters</span></div>
<div class="tags">
<span v-for="word in stats.top_words" :key="word" class="tag">{{ word }}</span>
</div>
</div>
</div>
<pre class="snippet"><code>const stats = await backend.word_stats({ text })</code></pre>
</section>
<section class="card">
<div class="card-header">
<span class="card-step">3</span>
<div>
<h2>Run jobs in the background</h2>
<p class="muted">Start long jobs without blocking the UI, then poll their status. Start several at once.</p>
</div>
</div>
<div class="card-body">
<div class="row">
<button @click="startJob">Start a job</button>
</div>
<div v-if="jobsError" class="output error">{{ jobsError }}</div>
<ul class="jobs">
<li v-if="jobs.length === 0" class="muted">No jobs yet.</li>
<li v-for="job in jobs" :key="job.id">
<span :class="['pill', job.status]">{{ job.status }}</span>
<code :title="job.id">{{ job.id.slice(-8) }}</code>
<span class="muted">{{ job.seconds }}s</span>
<div class="bar">
<div :class="['bar-fill', job.status]" :style="{ animationDuration: job.seconds + 's' }"></div>
</div>
</li>
</ul>
</div>
<pre class="snippet"><code>const id = await backendAsync.slow_task({ seconds })
const job = await getJob(id)</code></pre>
</section>
<section class="card">
<div class="card-header">
<span class="card-step">4</span>
<div>
<h2>Stream results</h2>
<p class="muted">A runnable can yield its output bit by bit. Render it as it arrives, like an LLM answer.</p>
</div>
</div>
<div class="card-body">
<div class="row">
<button @click="stream" :disabled="streaming">{{ streaming ? 'Streaming...' : 'Start streaming' }}</button>
</div>
<div class="output stream">
<span v-if="streamError" class="error">{{ streamError }}</span>
<template v-else-if="streamed || streaming">
{{ streamed }}<span v-if="streaming" class="caret"></span>
</template>
<span v-else class="muted">Streamed text shows up here.</span>
</div>
</div>
<pre class="snippet"><code>await streamJob(id, (u) => append(u.new_result_stream))</code></pre>
</section>
</div>
</main>
</template>
<script setup>
import { ref } from 'vue'
import { backend, backendAsync, getJob, streamJob } from './wmill'
// Windmill injects the viewer (absent for anonymous visitors) and the workspace.
const ctx = window.ctx ?? {}
const firstName = (ctx.ctx?.name || ctx.ctx?.username || '').split(' ')[0]
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
function errorMessage(e) {
return e?.message ?? (typeof e === 'string' ? e : JSON.stringify(e))
}
// 1. Call a backend runnable
const name = ref(firstName || 'world')
const greeting = ref()
const greetError = ref()
const greetLoading = ref(false)
async function greet() {
greetLoading.value = true
greetError.value = undefined
try {
greeting.value = await backend.greet({ name: name.value })
} catch (e) {
greetError.value = errorMessage(e)
}
greetLoading.value = false
}
// 2. Use any language
const text = ref(${JSON.stringify(STATS_SAMPLE_TEXT)})
const stats = ref()
const statsError = ref()
const statsLoading = ref(false)
async function analyze() {
statsLoading.value = true
statsError.value = undefined
try {
stats.value = await backend.word_stats({ text: text.value })
} catch (e) {
statsError.value = errorMessage(e)
}
statsLoading.value = false
}
// 3. Run jobs in the background
const jobs = ref([])
const jobsError = ref()
function setStatus(id, status) {
const row = jobs.value.find((r) => r.id === id)
if (row) row.status = status
}
async function startJob() {
jobsError.value = undefined
try {
const seconds = 2 + Math.floor(Math.random() * 4)
const id = await backendAsync.slow_task({ seconds })
jobs.value = [{ id, seconds, status: 'queued' }, ...jobs.value].slice(0, 4)
while (true) {
await sleep(500)
const job = await getJob(id)
if (job.type === 'CompletedJob') {
setStatus(id, job.success ? 'success' : 'failure')
return
}
setStatus(id, job.started_at ? 'running' : 'queued')
}
} catch (e) {
jobsError.value = errorMessage(e)
}
}
// 4. Stream results
const streamed = ref('')
const streamError = ref()
const streaming = ref(false)
async function stream() {
streamed.value = ''
streamError.value = undefined
streaming.value = true
try {
const id = await backendAsync.stream_text({})
await streamJob(id, (update) => {
if (update.new_result_stream) streamed.value += update.new_result_stream
})
} catch (e) {
streamError.value = errorMessage(e)
}
streaming.value = false
}
</script>`
const indexVue = `import { createApp } from 'vue'
import App from './App.vue'
import "./index.css";
createApp(App).mount('#root')`
const indexCss = `/* The app renders inside an iframe whose host page has its own theme the
iframe cannot read, so paint an opaque surface here: without one the host's
background shows through and dark text lands on it unreadable. */
@@ -1158,23 +934,10 @@ export const svelte5Template = {
}`
}
export const vueTemplate = {
'/index.ts': indexVue,
'/App.vue': appVue,
'/index.css': indexCss,
'/package.json': `{
"dependencies": {
"core-js": "3.26.1",
"vue": "3.5.13"
}
}`
}
export const FRAMEWORK_TEMPLATES = {
react19: react19Template,
react18: react18Template,
svelte5: svelte5Template,
vue: vueTemplate
svelte5: svelte5Template
}
export type FrameworkKey = keyof typeof FRAMEWORK_TEMPLATES
@@ -1551,171 +1314,6 @@ export async function main() {
}
}
export const appVueRouter = `
<template>
<div class="container">
<!-- Navigation tabs -->
<nav class="tabs">
<button
v-for="tab in tabs"
:key="tab.id"
:class="{ active: currentTab === tab.id }"
@click="changeTab(tab.id)"
>
{{ tab.name }}
</button>
</nav>
<!-- Content sections -->
<div class="content">
<div v-if="currentTab === 'home'" class="tab-content">
<h2>Home</h2>
<p>Welcome to the home tab!</p>
<!-- Nested navigation example -->
<div class="sub-nav">
<button
v-for="subItem in ['latest', 'popular']"
:key="subItem"
:class="{ active: currentSort === subItem }"
@click="changeSort(subItem)"
>
{{ subItem }}
</button>
</div>
</div>
<div v-else-if="currentTab === 'about'" class="tab-content">
<h2>About</h2>
<p>This is the about section</p>
</div>
<div v-else-if="currentTab === 'contact'" class="tab-content">
<h2>Contact</h2>
<p>Contact information here</p>
</div>
</div>
<!-- Debug info -->
<div class="debug">
<p>Current Tab: {{ currentTab }}</p>
<p>Current Sort: {{ currentSort }}</p>
<p>Current Hash: {{ currentHash }}</p>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
// Available tabs
const tabs = [
{ id: 'home', name: 'Home' },
{ id: 'about', name: 'About' },
{ id: 'contact', name: 'Contact' }
];
const currentTab = ref('home');
const currentSort = ref('latest');
const currentHash = ref('');
// Navigation functions
function changeTab(tabId) {
currentTab.value = tabId;
updateHash();
}
function changeSort(sort) {
currentSort.value = sort;
updateHash();
}
function updateHash() {
const params = new URLSearchParams();
params.set('tab', currentTab.value);
if (currentSort.value !== 'latest') {
params.set('sort', currentSort.value);
}
window.location.hash = params.toString();
currentHash.value = window.location.hash;
}
function parseHash() {
const params = new URLSearchParams(window.location.hash.slice(1));
currentTab.value = params.get('tab') || 'home';
currentSort.value = params.get('sort') || 'latest';
currentHash.value = window.location.hash;
}
// Hash change handler
function handleHashChange() {
parseHash();
}
onMounted(() => {
if (window.location.hash) {
parseHash();
} else {
updateHash();
}
window.addEventListener('hashchange', handleHashChange);
});
onUnmounted(() => {
window.removeEventListener('hashchange', handleHashChange);
});
</script>
<style scoped>
.container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.tabs {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
button {
padding: 8px 16px;
border: 1px solid #ddd;
background: #fff;
border-radius: 4px;
cursor: pointer;
}
button.active {
background: #4CAF50;
color: white;
border-color: #4CAF50;
}
.content {
padding: 20px;
border: 1px solid #ddd;
border-radius: 4px;
}
.tab-content {
margin-bottom: 20px;
}
.sub-nav {
margin-top: 10px;
}
.debug {
margin-top: 20px;
padding: 10px;
background: #f5f5f5;
border-radius: 4px;
font-size: 0.9em;
color: #666;
}
</style>`
export const appSvelteRouter = `
<script>
import { onMount, onDestroy } from 'svelte';
@@ -30,7 +30,7 @@ app related commands
- `app dev [app_folder:string]` - Start a development server for building apps with live reload and hot module replacement
- `--port <port:number>` - Port to run the dev server on (will find next available port if occupied)
- `--host <host:string>` - Host to bind the dev server to
- `--entry <entry:string>` - Entry point file (default: index.ts for Svelte/Vue, index.tsx otherwise)
- `--entry <entry:string>` - Entry point file (default: index.ts for Svelte, index.tsx otherwise)
- `--no-open` - Don't automatically open the browser
- `--recording` - Frame the app in a shell with a Record button, to capture a replayable session recording of the app under development
- `app lint [app_folder:string]` - Lint a raw app folder to validate structure and buildability
@@ -41,7 +41,7 @@ app related commands
- `app new` - create a new raw app from a template
- `--summary <summary:string>` - App summary (short description). Skips the prompt when provided. Triggers non-interactive mode.
- `--path <path:string>` - App path (e.g., f/folder/my_app or u/username/my_app). Skips the prompt when provided. Triggers non-interactive mode.
- `--framework <framework:string>` - Framework template: react19 | react18 | svelte5 | vue. Skips the prompt when provided. Triggers non-interactive mode.
- `--framework <framework:string>` - Framework template: react19 | react18 | svelte5. Skips the prompt when provided. Triggers non-interactive mode.
- `--datatable <datatable:string>` - Datatable to wire up. Without this flag in non-interactive mode, no datatable is configured.
- `--schema <schema:string>` - Schema to use with --datatable. If it doesn't exist yet, a sql_to_apply/ migration (CREATE SCHEMA IF NOT EXISTS) is written for it.
- `--overwrite` - Overwrite the target directory if it already exists, without prompting.
+6 -6
View File
@@ -890,7 +890,7 @@ def main(db: postgresql):
export const RAW_APP_BASE = `# Windmill Raw Apps
Raw apps let you build custom frontends with React, Svelte, or Vue that connect to Windmill backend runnables and datatables.
Raw apps let you build custom frontends with React or Svelte that connect to Windmill backend runnables and datatables.
## App shape
@@ -906,7 +906,7 @@ A new app gets the setup Windmill's own new-app dialog gives one: a framework, t
### Framework
Use the framework the user named. When they named none, follow what there is to follow: the app being edited, or the apps the request points at. With nothing to follow, use **React 19**, the recommended default, **without asking**. React 18, Svelte 5 and Vue are for when they are asked for.
Use the framework the user named. When they named none, follow what there is to follow: the app being edited, or the apps the request points at. With nothing to follow, use **React 19**, the recommended default, **without asking**. React 18 and Svelte 5 are for when they are asked for. These three are the only frameworks the raw app bundler builds. When the user asks to build or convert an app in another framework, write no code in it: say it is not available and offer these three instead.
### Data setup
@@ -939,7 +939,7 @@ An app the user just started from the new-app dialog is already set up: its file
### Entrypoint
The entrypoint is \`index.tsx\` for React and \`index.ts\` for Svelte and Vue. It is both the bundling entrypoint (the bundler is esbuild) and the **mount** entrypoint: the preview executes the bundle against an empty \`<div id="root">\` and auto-renders nothing, so the entrypoint must mount a top-level \`App\` itself. Keep the UI in \`App.tsx\` / \`App.svelte\` / \`App.vue\` and keep the entrypoint as the mount shim.
The entrypoint is \`index.tsx\` for React and \`index.ts\` for Svelte. It is both the bundling entrypoint (the bundler is esbuild) and the **mount** entrypoint: the preview executes the bundle against an empty \`<div id="root">\` and auto-renders nothing, so the entrypoint must mount a top-level \`App\` itself. Keep the UI in \`App.tsx\` / \`App.svelte\` and keep the entrypoint as the mount shim.
React (\`index.tsx\`):
@@ -951,7 +951,7 @@ import App from './App'
createRoot(document.getElementById('root')!).render(<App />)
\`\`\`
Svelte (\`index.ts\`): \`mount(App, { target: document.getElementById('root')! })\`. Vue (\`index.ts\`): \`createApp(App).mount('#root')\`.
Svelte (\`index.ts\`): \`mount(App, { target: document.getElementById('root')! })\`.
**Never replace the entrypoint with a bare component** (\`export default function App() { ... }\` and no mount call). A component that is defined but never mounted renders a blank screen with **no error thrown** — it never executes, so nothing reaches the console or the error overlay. If an app renders blank, check that the entrypoint still mounts \`App\` into \`#root\`.
@@ -3577,7 +3577,7 @@ app related commands
- \`app dev [app_folder:string]\` - Start a development server for building apps with live reload and hot module replacement
- \`--port <port:number>\` - Port to run the dev server on (will find next available port if occupied)
- \`--host <host:string>\` - Host to bind the dev server to
- \`--entry <entry:string>\` - Entry point file (default: index.ts for Svelte/Vue, index.tsx otherwise)
- \`--entry <entry:string>\` - Entry point file (default: index.ts for Svelte, index.tsx otherwise)
- \`--no-open\` - Don't automatically open the browser
- \`--recording\` - Frame the app in a shell with a Record button, to capture a replayable session recording of the app under development
- \`app lint [app_folder:string]\` - Lint a raw app folder to validate structure and buildability
@@ -3588,7 +3588,7 @@ app related commands
- \`app new\` - create a new raw app from a template
- \`--summary <summary:string>\` - App summary (short description). Skips the prompt when provided. Triggers non-interactive mode.
- \`--path <path:string>\` - App path (e.g., f/folder/my_app or u/username/my_app). Skips the prompt when provided. Triggers non-interactive mode.
- \`--framework <framework:string>\` - Framework template: react19 | react18 | svelte5 | vue. Skips the prompt when provided. Triggers non-interactive mode.
- \`--framework <framework:string>\` - Framework template: react19 | react18 | svelte5. Skips the prompt when provided. Triggers non-interactive mode.
- \`--datatable <datatable:string>\` - Datatable to wire up. Without this flag in non-interactive mode, no datatable is configured.
- \`--schema <schema:string>\` - Schema to use with --datatable. If it doesn't exist yet, a sql_to_apply/ migration (CREATE SCHEMA IF NOT EXISTS) is written for it.
- \`--overwrite\` - Overwrite the target directory if it already exists, without prompting.
@@ -35,7 +35,7 @@ app related commands
- `app dev [app_folder:string]` - Start a development server for building apps with live reload and hot module replacement
- `--port <port:number>` - Port to run the dev server on (will find next available port if occupied)
- `--host <host:string>` - Host to bind the dev server to
- `--entry <entry:string>` - Entry point file (default: index.ts for Svelte/Vue, index.tsx otherwise)
- `--entry <entry:string>` - Entry point file (default: index.ts for Svelte, index.tsx otherwise)
- `--no-open` - Don't automatically open the browser
- `--recording` - Frame the app in a shell with a Record button, to capture a replayable session recording of the app under development
- `app lint [app_folder:string]` - Lint a raw app folder to validate structure and buildability
@@ -46,7 +46,7 @@ app related commands
- `app new` - create a new raw app from a template
- `--summary <summary:string>` - App summary (short description). Skips the prompt when provided. Triggers non-interactive mode.
- `--path <path:string>` - App path (e.g., f/folder/my_app or u/username/my_app). Skips the prompt when provided. Triggers non-interactive mode.
- `--framework <framework:string>` - Framework template: react19 | react18 | svelte5 | vue. Skips the prompt when provided. Triggers non-interactive mode.
- `--framework <framework:string>` - Framework template: react19 | react18 | svelte5. Skips the prompt when provided. Triggers non-interactive mode.
- `--datatable <datatable:string>` - Datatable to wire up. Without this flag in non-interactive mode, no datatable is configured.
- `--schema <schema:string>` - Schema to use with --datatable. If it doesn't exist yet, a sql_to_apply/ migration (CREATE SCHEMA IF NOT EXISTS) is written for it.
- `--overwrite` - Overwrite the target directory if it already exists, without prompting.
@@ -17,7 +17,7 @@ You need three values to run the command:
1. **summary** — a short description of the app
2. **path** — the windmill path, e.g. `f/folder/my_app` or `u/username/my_app`
3. **framework** — one of `react19` (recommended), `react18`, `svelte5`, `vue`
3. **framework** — one of `react19` (recommended), `react18`, `svelte5`
**summary and path**: if the user's request did not supply them, ask. Do not guess values and do not invent paths.
@@ -89,7 +89,7 @@ my_app__raw_app/
├── DATATABLES.md # Database schemas (run 'wmill app generate-agents' to refresh)
├── raw_app.yaml # App configuration (summary, path, data settings)
├── index.tsx # Frontend entry point
├── App.tsx # Main React/Svelte/Vue component
├── App.tsx # Main React/Svelte component
├── index.css # Styles
├── package.json # Frontend dependencies
├── wmill.ts # Auto-generated backend type definitions (DO NOT EDIT)
@@ -247,7 +247,7 @@ For the rest, tell the user which command fits their intent and let them run it
# Windmill Raw Apps
Raw apps let you build custom frontends with React, Svelte, or Vue that connect to Windmill backend runnables and datatables.
Raw apps let you build custom frontends with React or Svelte that connect to Windmill backend runnables and datatables.
## App shape
@@ -263,7 +263,7 @@ A new app gets the setup Windmill's own new-app dialog gives one: a framework, t
### Framework
Use the framework the user named. When they named none, follow what there is to follow: the app being edited, or the apps the request points at. With nothing to follow, use **React 19**, the recommended default, **without asking**. React 18, Svelte 5 and Vue are for when they are asked for.
Use the framework the user named. When they named none, follow what there is to follow: the app being edited, or the apps the request points at. With nothing to follow, use **React 19**, the recommended default, **without asking**. React 18 and Svelte 5 are for when they are asked for. These three are the only frameworks the raw app bundler builds. When the user asks to build or convert an app in another framework, write no code in it: say it is not available and offer these three instead.
### Data setup
@@ -293,7 +293,7 @@ Then build what was chosen: the data table and schema become the app's `data.dat
### Entrypoint
The entrypoint is `index.tsx` for React and `index.ts` for Svelte and Vue. It is both the bundling entrypoint (the bundler is esbuild) and the **mount** entrypoint: the preview executes the bundle against an empty `<div id="root">` and auto-renders nothing, so the entrypoint must mount a top-level `App` itself. Keep the UI in `App.tsx` / `App.svelte` / `App.vue` and keep the entrypoint as the mount shim.
The entrypoint is `index.tsx` for React and `index.ts` for Svelte. It is both the bundling entrypoint (the bundler is esbuild) and the **mount** entrypoint: the preview executes the bundle against an empty `<div id="root">` and auto-renders nothing, so the entrypoint must mount a top-level `App` itself. Keep the UI in `App.tsx` / `App.svelte` and keep the entrypoint as the mount shim.
React (`index.tsx`):
@@ -305,7 +305,7 @@ import App from './App'
createRoot(document.getElementById('root')!).render(<App />)
```
Svelte (`index.ts`): `mount(App, { target: document.getElementById('root')! })`. Vue (`index.ts`): `createApp(App).mount('#root')`.
Svelte (`index.ts`): `mount(App, { target: document.getElementById('root')! })`.
**Never replace the entrypoint with a bare component** (`export default function App() { ... }` and no mount call). A component that is defined but never mounted renders a blank screen with **no error thrown** — it never executes, so nothing reaches the console or the error overlay. If an app renders blank, check that the entrypoint still mounts `App` into `#root`.
+2 -2
View File
@@ -12,7 +12,7 @@ You need three values to run the command:
1. **summary** — a short description of the app
2. **path** — the windmill path, e.g. `f/folder/my_app` or `u/username/my_app`
3. **framework** — one of `react19` (recommended), `react18`, `svelte5`, `vue`
3. **framework** — one of `react19` (recommended), `react18`, `svelte5`
**summary and path**: if the user's request did not supply them, ask. Do not guess values and do not invent paths.
@@ -84,7 +84,7 @@ my_app__raw_app/
├── DATATABLES.md # Database schemas (run 'wmill app generate-agents' to refresh)
├── raw_app.yaml # App configuration (summary, path, data settings)
├── index.tsx # Frontend entry point
├── App.tsx # Main React/Svelte/Vue component
├── App.tsx # Main React/Svelte component
├── index.css # Styles
├── package.json # Frontend dependencies
├── wmill.ts # Auto-generated backend type definitions (DO NOT EDIT)
+4 -4
View File
@@ -1,6 +1,6 @@
# Windmill Raw Apps
Raw apps let you build custom frontends with React, Svelte, or Vue that connect to Windmill backend runnables and datatables.
Raw apps let you build custom frontends with React or Svelte that connect to Windmill backend runnables and datatables.
## App shape
@@ -16,7 +16,7 @@ A new app gets the setup Windmill's own new-app dialog gives one: a framework, t
### Framework
Use the framework the user named. When they named none, follow what there is to follow: the app being edited, or the apps the request points at. With nothing to follow, use **React 19**, the recommended default, **without asking**. React 18, Svelte 5 and Vue are for when they are asked for.
Use the framework the user named. When they named none, follow what there is to follow: the app being edited, or the apps the request points at. With nothing to follow, use **React 19**, the recommended default, **without asking**. React 18 and Svelte 5 are for when they are asked for. These three are the only frameworks the raw app bundler builds. When the user asks to build or convert an app in another framework, write no code in it: say it is not available and offer these three instead.
### Data setup
@@ -54,7 +54,7 @@ An app the user just started from the new-app dialog is already set up: its file
### Entrypoint
The entrypoint is `index.tsx` for React and `index.ts` for Svelte and Vue. It is both the bundling entrypoint (the bundler is esbuild) and the **mount** entrypoint: the preview executes the bundle against an empty `<div id="root">` and auto-renders nothing, so the entrypoint must mount a top-level `App` itself. Keep the UI in `App.tsx` / `App.svelte` / `App.vue` and keep the entrypoint as the mount shim.
The entrypoint is `index.tsx` for React and `index.ts` for Svelte. It is both the bundling entrypoint (the bundler is esbuild) and the **mount** entrypoint: the preview executes the bundle against an empty `<div id="root">` and auto-renders nothing, so the entrypoint must mount a top-level `App` itself. Keep the UI in `App.tsx` / `App.svelte` and keep the entrypoint as the mount shim.
React (`index.tsx`):
@@ -66,7 +66,7 @@ import App from './App'
createRoot(document.getElementById('root')!).render(<App />)
```
Svelte (`index.ts`): `mount(App, { target: document.getElementById('root')! })`. Vue (`index.ts`): `createApp(App).mount('#root')`.
Svelte (`index.ts`): `mount(App, { target: document.getElementById('root')! })`.
**Never replace the entrypoint with a bare component** (`export default function App() { ... }` and no mount call). A component that is defined but never mounted renders a blank screen with **no error thrown** — it never executes, so nothing reaches the console or the error overlay. If an app renders blank, check that the entrypoint still mounts `App` into `#root`.