Files
2026-08-03 18:09:06 +02:00

254 lines
8.0 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))
)
/**
* Fail the build if the emitted chunks import each other in a cycle.
*
* A chunk's imports all evaluate before its own body, so in a cycle one member runs
* while another has initialized nothing: its `const`/`class` exports read as
* `undefined` and the app dies on load with "X is not a constructor" behind
* SvelteKit's default 500 page. The module graph being acyclic is not enough —
* chunk *grouping* alone can create one. See docs/frontend-import-cycles.md.
*/
function assertAcyclicChunks() {
return {
name: 'wm-assert-acyclic-chunks',
generateBundle(_options, bundle) {
const imports = new Map()
for (const [file, chunk] of Object.entries(bundle)) {
if (chunk.type === 'chunk') imports.set(file, chunk.imports ?? [])
}
const state = new Map() // file -> 'visiting' | 'done'
const describe = (file) => {
const chunk = bundle[file]
const ids = chunk.moduleIds ?? Object.keys(chunk.modules ?? {})
const own = ids.filter((id) => id.includes('/src/')).map((id) => id.split('/src/')[1])
const shown = own.slice(0, 6).join(', ')
return ` ${file}\n ${shown}${own.length > 6 ? `, +${own.length - 6} more` : ''}`
}
for (const root of imports.keys()) {
if (state.get(root)) continue
const stack = [[root, 0]]
const path = [root]
state.set(root, 'visiting')
while (stack.length) {
const frame = stack[stack.length - 1]
const deps = imports.get(frame[0]) ?? []
if (frame[1] >= deps.length) {
state.set(path.pop(), 'done')
stack.pop()
continue
}
const dep = deps[frame[1]++]
if (state.get(dep) === 'visiting') {
const cycle = path.slice(path.indexOf(dep)).concat(dep)
this.error(
`Cyclic chunk imports — this ships a runtime crash, see docs/frontend-import-cycles.md:\n` +
cycle.map(describe).join('\n ->\n')
)
}
if (!state.has(dep) && imports.has(dep)) {
state.set(dep, 'visiting')
path.push(dep)
stack.push([dep, 0])
}
}
}
}
}
}
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'
// Cross-origin isolation headers, scoped to mirror the production predicate —
// see `needs_cross_origin_isolation` in backend/windmill-api/src/static_assets.rs
// for which paths need them and why the raw app viewer must be excluded.
// `enforce: 'pre'` so these headers are set before SvelteKit's sirv static
// handler serves `static/` files and ends the response without calling next().
function needsCrossOriginIsolation(url) {
const [path, query = ''] = url.split('?')
return (
path.startsWith('/apps_raw/edit') ||
path.startsWith('/apps_raw/add') ||
path.startsWith('/ui_builder/') ||
((path.startsWith('/public/') || path.startsWith('/a/')) &&
new URLSearchParams(query).has('wm_coep'))
)
}
let plugin = {
name: 'configure-response-headers',
enforce: 'pre',
configureServer: (server) => {
server.middlewares.use((req, res, next) => {
if (needsCrossOriginIsolation(req.url ?? '')) {
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin')
res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp')
}
// CORP on everything so dev assets stay loadable as subresources of
// isolated documents on other dev origins (e.g. 127.0.0.1 vs localhost).
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,
assertAcyclicChunks()
],
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'],
build: {
rollupOptions: {
output: {
// src/lib/gen is a self-contained generated client and a leaf of the module
// graph. Left alone the bundler splits it — index.ts in one chunk, the
// 12k-line schemas.gen.ts in another beside app code that imports back —
// which makes the *chunk* graph cyclic where the module graph is not, and
// modules then evaluate against uninitialized bindings. See
// docs/frontend-import-cycles.md.
advancedChunks: { groups: [{ name: 'gen', test: /[\\/]src[\\/]lib[\\/]gen[\\/]/ }] }
}
}
},
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}', 'src/**/*.dom.{test,spec}.{js,ts}'],
setupFiles: ['src/lib/test-setup.ts']
}
},
{
// `*.dom.test.ts` — for the pure DOM utilities (snapshot serialization,
// replay sanitization) whose contracts can only be asserted against a
// real document.
extends: './vite.config.js',
test: {
name: 'dom',
environment: 'jsdom',
include: ['src/**/*.dom.{test,spec}.{js,ts}']
}
}
]
}
}
export default config