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 }) }, } }