forked from linkong/planet
71 lines
3.1 KiB
JavaScript
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 })
|
|
},
|
|
}
|
|
}
|