import { createReadStream, existsSync, readdirSync, statSync, watch } from 'node:fs' import { mkdir } from 'node:fs/promises' import { createServer } from 'node:http' import { extname, join, normalize, relative, resolve, sep } from 'node:path' import { spawn } from 'node:child_process' const root = resolve(new URL('..', import.meta.url).pathname) const dist = join(root, 'dist') const watchOnly = process.argv.includes('--watch-only') const port = Number(process.env.PLANET_PREVIEW_PORT || process.env.PORT || 4173) const clients = new Set() let buildTimer = null let building = false let pending = false const mime = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8', '.json': 'application/json; charset=utf-8', '.svg': 'image/svg+xml', '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.webp': 'image/webp', '.ico': 'image/x-icon', '.woff': 'font/woff', '.woff2': 'font/woff2', } function safePath(urlPath) { const clean = decodeURIComponent(urlPath.split('?')[0] || '/') const target = normalize(join(dist, clean)) return target.startsWith(dist + sep) || target === dist ? target : join(dist, 'index.html') } function runBuild() { if (building) { pending = true return Promise.resolve(false) } building = true pending = false console.log('[preview:auto] building dist...') return new Promise((resolveBuild) => { const runtime = process.versions.bun ? process.execPath : (process.env.BUN_PATH || 'bun') const child = spawn(runtime, ['run', 'build'], { cwd: root, stdio: 'inherit', shell: process.platform === 'win32' }) child.on('exit', (code) => { building = false if (code === 0) { console.log('[preview:auto] build complete') broadcastReload() resolveBuild(true) } else { console.error(`[preview:auto] build failed with code ${code}`) resolveBuild(false) } if (pending) void runBuild() }) }) } function scheduleBuild(file = '') { clearTimeout(buildTimer) buildTimer = setTimeout(() => { console.log(`[preview:auto] change detected${file ? `: ${relative(root, file)}` : ''}`) void runBuild() }, 180) } function watchPath(path) { if (!existsSync(path)) return try { watch(path, { recursive: true }, (_event, fileName) => { if (!fileName) return scheduleBuild(path) const file = join(path, String(fileName)) if (file.includes(`${sep}node_modules${sep}`) || file.includes(`${sep}dist${sep}`)) return scheduleBuild(file) }) } catch { watch(path, () => scheduleBuild(path)) if (statSync(path).isDirectory()) { for (const entry of readdirSync(path, { withFileTypes: true })) { if (!entry.isDirectory() || entry.name === 'node_modules' || entry.name === 'dist') continue watchPath(join(path, entry.name)) } } } } function broadcastReload() { for (const response of clients) { response.write('event: reload\ndata: now\n\n') } } function injectReloadClient(html) { if (watchOnly) return html const client = `` return html.includes('') ? html.replace('', `${client}`) : `${html}${client}` } function serveFile(request, response) { if (request.url === '/__preview_reload') { response.writeHead(200, { 'content-type': 'text/event-stream', 'cache-control': 'no-cache', connection: 'keep-alive', }) response.write('\n') clients.add(response) request.on('close', () => clients.delete(response)) return } let file = safePath(request.url || '/') if (!existsSync(file) || statSync(file).isDirectory()) file = join(file, 'index.html') if (!existsSync(file)) file = join(dist, 'index.html') if (!existsSync(file)) { response.writeHead(404) response.end('Run bun run build first.') return } const ext = extname(file) response.setHeader('content-type', mime[ext] || 'application/octet-stream') response.setHeader('cache-control', 'no-store') if (ext === '.html') { let html = '' createReadStream(file, 'utf8') .on('data', (chunk) => { html += chunk }) .on('end', () => response.end(injectReloadClient(html))) .on('error', () => { response.writeHead(500) response.end('Failed to read preview file.') }) return } createReadStream(file).pipe(response) } await mkdir(dist, { recursive: true }) await runBuild() watchPath(join(root, 'src')) watchPath(join(root, 'public')) for (const file of ['index.html', 'vite.config.mts', 'tailwind.config.ts', 'postcss.config.cjs', 'package.json']) { watchPath(join(root, file)) } if (watchOnly) { console.log('[preview:auto] watching source changes; press Ctrl+C to stop') } else { createServer(serveFile).listen(port, () => { console.log(`[preview:auto] serving http://localhost:${port}`) console.log('[preview:auto] build success will reload connected pages') }) }