132 lines
4.8 KiB
TypeScript
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',
|
|
},
|
|
},
|
|
})
|