The pull loop (entry.sh) runs `git reset --hard` straight into the tree Vite watches. A landing rewrites hundreds of files one by one, and Vite answered the first of them while git was still writing the rest: an open page was reloaded onto a mix of old and new modules, and its tab clicks then fetched nothing until it was reloaded by hand. deployment/cloud/atomic-pull.mjs, added to the cloud overlay only (local dev, builds and entry.sh are unchanged; Vite restarts itself once when this config lands). The first file event opens a gate that closes once the tree has been quiet for ATOMIC_QUIET_MS (1200), never past ATOMIC_CEILING_MS (20000, under oauth2-proxy's 30 s upstream timeout). While it is open it holds the messages to the browsers (update, full-reload, prune, error: at ws.send, which Vite 5's server.hot broadcasts through and which IS environments.client.hot in Vite 6/7), every request except the backend routes (a middleware registered ahead of all others), and a config restart. When it closes: invalidate the module graph, release the requests, then exactly ONE full-reload (or the held restart). A held error is dropped, so none is buffered for the next browser. Past the ceiling it passes everything through until the tree is quiet. ATOMIC_QUIET_MS=0 or PULL_INTERVAL=0 turns it off. Measured on a scratch Vite 5.4.21 project (the version bun.lock pins) under a copy of this overlay, with a page of 50 modules in a browser: one `git reset --hard` of 312 files sent the page 27 full-reloads and 25 updates without the plugin, and exactly 1 full-reload with every module new with it. Bursts, a request mid-burst, a single edit, a broken-then-fixed module (page open and no page open), a config restart (and a failed one) and the ceiling: 27/27 checks pass.
94 lines
4.8 KiB
JavaScript
94 lines
4.8 KiB
JavaScript
// Overlay for the cloud dev server: the repo's own vite.config.js plus what a shared, public
|
|
// host needs. Every value comes from the environment (see deployment/cloud/README.md).
|
|
import base from '/app/repo/vite.config.js'
|
|
import { mergeConfig } from 'vite'
|
|
import { readFileSync } from 'node:fs'
|
|
import { GATED_PROFILES } from '/app/repo/internal/_tree-name.mjs'
|
|
import { atomicPull } from './atomic-pull.mjs'
|
|
|
|
const env = process.env
|
|
const SETUP_STATE = '/tmp/rudder-setup.state' // written by entry.sh: running -> done | failed
|
|
const setupState = () => { try { return readFileSync(SETUP_STATE, 'utf8').trim() } catch { return '' } }
|
|
|
|
// Rudder's setup runs in the background (entry.sh), so the site serves before the engine exists. While it
|
|
// runs, /api/rudder answers "starting" (the widget shows "Loading speech model…"; /events is held open so it
|
|
// does not retry in a loop). When setup ends, the dev server restarts ONCE: the repo's backend plugin only
|
|
// looks for the engine binary at start, and the restart is what starts it. 'pre' puts this ahead of that
|
|
// plugin's own "engine offline" answer.
|
|
const rudderStarting = {
|
|
name: 'cloud-rudder-starting',
|
|
enforce: 'pre',
|
|
apply: 'serve',
|
|
configureServer(server) {
|
|
if (setupState() !== 'running') return
|
|
server.middlewares.use('/api/rudder', (req, res, next) => {
|
|
if (setupState() !== 'running') return next()
|
|
const status = { engine: 'starting', error: '', audio: 'browser' }
|
|
if ((req.url || '').startsWith('/events')) {
|
|
res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive' })
|
|
res.write(`data: ${JSON.stringify({ type: 'status', status })}\n\n`)
|
|
return
|
|
}
|
|
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
res.end(JSON.stringify(status))
|
|
})
|
|
const poll = setInterval(() => {
|
|
if (setupState() === 'running') return
|
|
clearInterval(poll)
|
|
server.config.logger.info(`[rudder] setup ${setupState() || 'ended'}: restarting the dev server to start the engine`)
|
|
server.restart()
|
|
}, 2000)
|
|
server.httpServer?.once('close', () => clearInterval(poll))
|
|
},
|
|
}
|
|
|
|
// ACCESS=public (entry.sh sets DX_PUBLIC_STUDIO=1): Vite owns the public port with no sign-in in front of it.
|
|
// - The backend's paths go to admin.mjs, whose gates decide: /cdn/ and /api-prod/api/ public, /pb/ records
|
|
// by PocketBase's own rules, and /pb/_/, _superusers and /admin/ for the admin password.
|
|
// - GATED bundles (gatedProfiles, e.g. key-cities) must never reach a public surface. The Studio already leaves
|
|
// them out of its list; here their source and dev pages are refused too, so a typed URL can't load them. So is
|
|
// /__plugin-gallery/registry.json, the raw discovery list for smoke tools: it still names the gated bundles and
|
|
// their profiles, and the Studio itself reads only manifest.json.
|
|
// - /__open-in-editor (Vite's own) would start an editor process on this server for a visitor: refused.
|
|
const PUBLIC = env.DX_PUBLIC_STUDIO === '1'
|
|
const publicGuard = {
|
|
name: 'cloud-public-guard',
|
|
enforce: 'pre',
|
|
apply: 'serve',
|
|
configureServer(server) {
|
|
if (!PUBLIC) return
|
|
server.middlewares.use((req, res, next) => {
|
|
let p = (req.url || '').split('?')[0]
|
|
try { p = decodeURIComponent(p) } catch { /* keep it raw */ }
|
|
const gated = GATED_PROFILES.some((g) => p.includes(`/plugin-designs/${g}/`) || p === `/__leaf/${g}` || p.startsWith(`/__leaf/${g}/`))
|
|
if (!gated && !p.startsWith('/__open-in-editor') && p !== '/__plugin-gallery/registry.json') return next()
|
|
res.statusCode = 404
|
|
res.setHeader('content-type', 'text/plain; charset=utf-8')
|
|
res.end('not found\n')
|
|
})
|
|
},
|
|
}
|
|
|
|
export default async (e) => {
|
|
const b = typeof base === 'function' ? await base(e) : base
|
|
const hmr = env.HMR_CLIENT_PORT
|
|
? { clientPort: Number(env.HMR_CLIENT_PORT), protocol: env.HMR_PROTOCOL || 'wss' }
|
|
: undefined
|
|
return mergeConfig(b, {
|
|
// atomicPull: the pull loop's `git reset --hard` reaches the browsers as ONE reload once the tree is complete,
|
|
// never half-written (atomic-pull.mjs). Its request gate goes ahead of every other middleware.
|
|
plugins: [atomicPull(), publicGuard, rudderStarting],
|
|
cacheDir: '/tmp/vite-cache',
|
|
server: {
|
|
host: env.VITE_HOST || '0.0.0.0',
|
|
port: Number(env.VITE_PORT || 5173),
|
|
strictPort: true,
|
|
allowedHosts: true,
|
|
...(hmr ? { hmr } : {}),
|
|
...(PUBLIC ? { proxy: { '^/(cdn|api-prod|pb|admin)/': { target: 'http://127.0.0.1:5175', ws: true } } } : {}),
|
|
// Public: the checkout's git internals are not served either (the private history, .git/config). Vite's
|
|
// defaults are restated because setting deny replaces them.
|
|
...(PUBLIC ? { fs: { strict: true, deny: ['.env', '.env.*', '*.{crt,pem}', '**/.git/**'] } } : {}),
|
|
},
|
|
})
|
|
}
|