Files
planet/frontend/scripts/preview-auto.mjs
linkong fbca381512
Some checks failed
ci / backend (push) Has been cancelled
ci / frontend (push) Has been cancelled
ci / delivery (push) Has been cancelled
release / images (push) Has been cancelled
release: bump version to 0.62.0
2026-05-21 01:37:32 +08:00

162 lines
5.0 KiB
JavaScript

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 = `<script type="module">
const source = new EventSource('/__preview_reload');
source.addEventListener('reload', () => window.location.reload());
</script>`
return html.includes('</body>') ? html.replace('</body>', `${client}</body>`) : `${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')
})
}