release: bump version to 0.62.0
This commit is contained in:
161
frontend/scripts/preview-auto.mjs
Normal file
161
frontend/scripts/preview-auto.mjs
Normal file
@@ -0,0 +1,161 @@
|
||||
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')
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user