Files
windmill/frontend/vite.config.js
Guilhem a116715c41 feat(frontend): restore raw app 'open preview in separate window' (#9765)
* feat(frontend): restore raw app 'open preview in separate window'

Re-adds the detached preview window dropped when preview hosting moved to
the host (ui-builder f52d8e5b). Live-synced preview + dark mode, and wires
the runnable bridge to the detached window so backend calls resolve there.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(frontend): repaint detached raw app preview after refresh

The detached preview tab is a blank app-preview.html shell fed by the
editor over postMessage. A one-shot opener load listener can't survive the
tab refreshing itself, so a manual reload left it blank. It now posts
'appPreviewReady' on every (re)load and the editor re-sends the build; the
orphaned window is also closed when the editor unmounts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(frontend): keep load-based feed for detached preview initial open

Relying solely on the appPreviewReady handshake left the detached window
blank on first open against app-preview.html artifacts that predate the
handshake (the pinned UI Builder tarball). Restore the one-shot load feed so
initial open works regardless of the served shell; the handshake still
covers manual refresh.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(frontend): re-sync dark mode when refocusing detached preview

The focus-reuse path replayed the build but not the theme, so re-opening an
existing detached window after a dark-mode toggle kept the stale theme until
the next build. Extract a feedExternalPreview() helper (theme + build) used by
the open, focus-reuse, load and handshake paths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(frontend): bump ui_builder artifact to 062d11c (preview handshake)

Pins the UI Builder artifact built from windmill-code-ui-builder#14, which
adds the appPreviewReady handshake, detached-preview favicon and title.
Activates refresh-repaint + favicon for the detached raw app preview.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(frontend): serve ui_builder static bundle in dev, proxy :4000 only as fallback

The postinstall downloads the pinned UI Builder artifact into static/ui_builder,
which SvelteKit already serves at /ui_builder. Skip the :4000 proxy when that
bundle is present so dev uses it directly (matching prod / the backend's
static-vs-:4000 fallback); no separate UI Builder dev server needed. Delete
static/ui_builder to develop the builder against :4000.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(frontend): harden detached preview origin + scope window name

Addresses cubic review on #9765:
- P1: only honor appPreviewReady from a same-origin sender and post the build
  with targetOrigin=location.origin, so user app code that navigates the
  detached window cross-origin can't trigger/receive a build (app source).
- P2: scope the detached window name per app path so two open editors don't
  collide over one OS-level preview window.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 15:39:16 +00:00

150 lines
4.3 KiB
JavaScript

import { sveltekit } from '@sveltejs/kit/vite'
import { existsSync, readFileSync } from 'fs'
import { fileURLToPath } from 'url'
import mkcert from 'vite-plugin-mkcert'
const file = fileURLToPath(new URL('package.json', import.meta.url))
const json = readFileSync(file, 'utf8')
const version = JSON.parse(json)
// The postinstall downloads the pinned UI Builder artifact into static/ui_builder,
// which SvelteKit serves at /ui_builder. Serve that directly; only proxy to a
// live UI Builder dev server on :4000 when the bundle is absent (mirrors the
// backend's static-vs-:4000 fallback). Delete static/ui_builder to develop the
// builder against :4000.
const uiBuilderStaticPresent = existsSync(
fileURLToPath(new URL('static/ui_builder/app-preview.html', import.meta.url))
)
const remoteUrl =
process.env.REMOTE ??
(process.env.BACKEND_PORT
? `http://localhost:${process.env.BACKEND_PORT}`
: 'https://app.windmill.dev/')
const cookieDomain = process.env.ISOLATE_DEV_AUTH === '1' ? '' : 'localhost'
// `enforce: 'pre'` so these headers are set before SvelteKit's sirv static
// handler serves `static/` files and ends the response without calling next().
let plugin = {
name: 'configure-response-headers',
enforce: 'pre',
configureServer: (server) => {
server.middlewares.use((_req, res, next) => {
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin')
res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp')
res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin')
next()
})
}
}
/** @type {import('vite').UserConfig} */
const config = {
server: {
https: process.env.HTTPS === 'true',
allowedHosts: [
'localhost',
'127.0.0.1',
'0.0.0.0',
'rubendev.wimill.xyz',
'windmill.xyz',
'app.windmill.xyz',
'public.windmill.xyz'
],
port: parseInt(process.env.FRONTEND_PORT) || 3000,
cors: { origin: '*' },
proxy: {
'^/\\.well-known/.*': {
target: remoteUrl,
changeOrigin: true,
cookieDomainRewrite: cookieDomain
},
'^/api/w/[^/]+/s3_proxy/.*': {
target: remoteUrl,
changeOrigin: false, // Important for signature to be correct
cookieDomainRewrite: cookieDomain,
configure: (proxy, options) => {
proxy.on('proxyReq', (proxyReq, req, res) => {
// Prevent collapsing slashes during URL normalization
const originalPath = req.url
proxyReq.path = originalPath
})
}
},
'^/api/.*': {
target: remoteUrl,
changeOrigin: true,
cookieDomainRewrite: cookieDomain
},
'^/ws/.*': {
target: process.env.REMOTE_LSP ?? process.env.REMOTE_EXTRA ?? 'https://app.windmill.dev',
changeOrigin: true,
ws: true
},
'^/ws_mp/.*': {
target: process.env.REMOTE_MP ?? process.env.REMOTE_EXTRA ?? 'https://app.windmill.dev',
changeOrigin: true,
ws: true
},
'^/ws_debug/.*': {
target: process.env.REMOTE_DEBUG ?? process.env.REMOTE_EXTRA ?? 'https://app.windmill.dev',
changeOrigin: true,
ws: true
},
...(uiBuilderStaticPresent
? {}
: {
'^/ui_builder/.*': {
target: 'http://localhost:4000',
changeOrigin: true,
headers: {
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp',
'Cross-Origin-Resource-Policy': 'cross-origin'
}
}
})
}
},
preview: { port: 3001 },
plugins: [sveltekit(), ...(process.env.HTTPS === 'true' ? [mkcert()] : []), plugin],
define: { __pkg__: version },
optimizeDeps: {
include: ['highlight.js', 'highlight.js/lib/core', 'monaco-vim'],
exclude: [
'@codingame/monaco-vscode-standalone-typescript-language-features',
'@codingame/monaco-vscode-standalone-languages',
'windmill-client'
]
},
worker: { format: 'es' },
resolve: {
alias: {
path: 'path-browserify',
'monaco-editor/esm/vs/editor/contrib/hover/browser/hover':
'monaco-editor/esm/vs/editor/contrib/hover/browser/hoverContribution'
},
dedupe: ['vscode', 'monaco-editor']
},
assetsInclude: ['**/*.wasm'],
test: {
expect: { requireAssertions: true },
projects: [
{
extends: './vite.config.js',
test: {
name: 'server',
environment: 'node',
include: ['src/**/*.{test,spec}.{js,ts}'],
exclude: ['src/**/*.svelte.{test,spec}.{js,ts}'],
setupFiles: ['src/lib/test-setup.ts']
}
}
]
}
}
export default config