Files
planet/frontend/scripts/earth-runtime-plugin.mjs
2026-09-23 21:20:12 +08:00

71 lines
3.1 KiB
JavaScript

import { createHash } from 'node:crypto'
import { readFile } from 'node:fs/promises'
import { basename, join } from 'node:path'
// Earth remains a standalone public page with its pinned import map. Bundle
// only project modules, retaining the /earth/js/ base for assets and workers.
export function earthRuntimePlugin(root) {
const earth = join(root, 'public/earth')
let compilation = null
async function compile() {
if (compilation) return compilation
compilation = (async () => {
const result = await Bun.build({
entrypoints: [join(earth, 'js/main.js'), join(earth, 'js/earth-preload.js')],
target: 'browser', format: 'esm', splitting: true, packages: 'external', minify: true,
naming: { entry: 'runtime-[name]-[hash].js', chunk: 'runtime-[hash].js' },
})
if (!result.success) throw new AggregateError(result.logs, 'Earth runtime build failed')
const assets = new Map(await Promise.all(result.outputs.map(async output => {
const source = await output.text()
return [basename(output.path), { source,
etag: `"${createHash('sha256').update(source).digest('hex')}"` }]
})))
let html = await readFile(join(earth, 'index.html'), 'utf8')
for (const entry of ['main', 'earth-preload']) {
const filename = [...assets.keys()].find(name => name.startsWith(`runtime-${entry}-`))
html = html.replace(new RegExp(`src="(?:\\./)?js/${entry}\\.js"`), `src="./js/${filename}"`)
}
return { assets, html }
})().catch(error => { compilation = null; throw error })
return compilation
}
return {
name: 'earth-runtime',
configureServer(server) {
server.watcher.on('change', file => {
if (file.startsWith(earth) && (file.endsWith('.js') || file === join(earth, 'index.html'))) {
compilation = null
}
})
server.middlewares.use(async (req, res, next) => {
const url = (req.url || '').split('?')[0]
const isPage = url === '/earth/index.html' || url === '/earth/'
const isRuntime = /^\/earth\/js\/runtime-[\w-]+\.js$/.test(url)
if (!isPage && !isRuntime) return next()
try {
const { assets, html } = await compile()
const asset = assets.get(basename(url))
if (!isPage && !asset) return next()
res.setHeader('Cache-Control', 'no-cache')
if (asset) {
res.setHeader('ETag', asset.etag)
if (req.headers['if-none-match'] === asset.etag) {
res.statusCode = 304; res.end(); return
}
}
res.setHeader('Content-Type', isPage ? 'text/html; charset=utf-8' : 'text/javascript; charset=utf-8')
res.end(isPage ? html : asset.source)
} catch (error) { next(error) }
})
},
async generateBundle() {
const { assets, html } = await compile()
for (const [name, asset] of assets) {
this.emitFile({ type: 'asset', fileName: `earth/js/${name}`, source: asset.source })
}
this.emitFile({ type: 'asset', fileName: 'earth/index.html', source: html })
},
}
}