Files
804d8ad4-7bc2-401c-984b-05f…/vite.config.ts
kudinDmitriyUp 7a379725ec Initial commit
2026-05-08 19:27:02 +00:00

132 lines
4.8 KiB
TypeScript

import { defineConfig, type Plugin } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
import path from 'path'
import * as fs from 'fs'
// In-sandbox bridge for Bob-AI's post-commit render check.
//
// On every HMR update (and on first mount) the iframed app POSTs a JSON
// payload to `/__webild/render-status` describing whether the page actually
// rendered (`{ok:true, rootChildren, bodyTextLen}`) or failed
// (`{ok:false, reason, error, stack, ...}`). This middleware is the dev-only
// receiver: it writes the latest payload to `/tmp/webild-render-status-<port>.json`
// inside the sandbox filesystem.
//
// Bob-AI does NOT receive these POSTs directly — it reads the file from the
// sandbox via the E2B SDK after each commit. This avoids needing a public
// callback endpoint and keeps the probe a no-op on production deploys (the
// app is never iframed there, so reportRenderStatus short-circuits).
//
// `apply: 'serve'` keeps this out of `vite build`. Endpoint paths intentionally
// short-circuit before any 404/SPA handler kicks in.
function webildRenderStatusPlugin(): Plugin {
return {
name: 'webild-render-status',
apply: 'serve',
configureServer(server) {
const port = server.config.server?.port ?? 3000
const statusFile = `/tmp/webild-render-status-${port}.json`
server.middlewares.use((req, res, next) => {
if (!req.url || !req.url.startsWith('/__webild/render-status')) return next()
try {
if (req.method === 'POST') {
const chunks: Buffer[] = []
req.on('data', (chunk: Buffer) => {
chunks.push(chunk)
// hard cap to avoid unbounded memory if a probe goes haywire
if (chunks.reduce((n, c) => n + c.length, 0) > 64 * 1024) {
res.statusCode = 413
res.end('payload too large')
req.destroy()
}
})
req.on('end', () => {
try {
const body = Buffer.concat(chunks).toString('utf8').trim() || '{}'
JSON.parse(body)
fs.writeFileSync(statusFile, body, 'utf8')
res.statusCode = 204
res.end()
} catch (err) {
res.statusCode = 400
res.setHeader('Content-Type', 'text/plain')
res.end(String((err as Error)?.message || err))
}
})
req.on('error', () => {
if (!res.headersSent) {
res.statusCode = 500
res.end()
}
})
return
}
if (req.method === 'GET') {
try {
const content = fs.readFileSync(statusFile, 'utf8')
res.statusCode = 200
res.setHeader('Content-Type', 'application/json')
res.end(content)
} catch {
res.statusCode = 404
res.end()
}
return
}
if (req.method === 'DELETE') {
try {
fs.unlinkSync(statusFile)
} catch {
// already absent
}
res.statusCode = 204
res.end()
return
}
res.statusCode = 405
res.end()
} catch (err) {
if (!res.headersSent) {
res.statusCode = 500
res.end(String((err as Error)?.message || err))
}
}
})
},
}
}
export default defineConfig({
plugins: [react(), tailwindcss(), webildRenderStatusPlugin()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
server: {
port: 3000,
host: true,
// CRITICAL for the e2b shared pool: forbid Vite's default behaviour of
// auto-picking another port on EADDRINUSE. The pool maps each port to a
// specific projectSlug; if Vite for project A silently lands on the slot
// belonging to project B, /webild-ready.json on the e2b host responds
// with B's project id and the frontend sanity check fails (slugMismatch),
// or worse, the iframe loads B's preview instead of A's. Better to crash
// loudly so the outer while-loop + stop-sentinel can clean up.
strictPort: true,
// E2B exposes each port on `<port>-<sandboxId>.sandbox.webild.io`. Vite 8's
// leading-dot wildcard (`.sandbox.webild.io`) refuses some patterns like
// `<port>-<sandboxId>.sandbox.webild.io` with 403. These are dev-only
// sandboxes — host check has no security value, so disable it entirely.
allowedHosts: true,
hmr: {
// Browser connects via the same e2b-proxied https host on port 443,
// but the dev server itself listens on raw `port`. Without this Vite
// tells the client to open `wss://localhost:3000` and HMR fails.
clientPort: 443,
protocol: 'wss',
},
},
})